This commit is contained in:
k
2026-09-21 23:10:55 +02:00
parent aaf9354b7f
commit 2974105e25
4 changed files with 37 additions and 11 deletions
+8 -8
View File
@@ -25,11 +25,11 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport, isFaile
window.open(`https://duckduckgo.com/?q=${encodeURIComponent(msg.content.slice(0, 400))}`, "_blank", "noopener,noreferrer");
}
return (
<div className={cn("flex gap-3", isUser && "flex-row-reverse")}>
<div className={cn("flex gap-2 sm:gap-3", isUser && "flex-row-reverse")}>
{isUser
? <Avatar name={authorName} url={authorAvatar} size={32} />
: <AssistantAvatar size={32} />}
<div className={cn("min-w-0 max-w-[85%]", isUser && "flex flex-col items-end")}>
<div className={cn("min-w-0 max-w-[88%] sm:max-w-[85%]", isUser && "flex flex-col items-end")}>
<div className="mb-1 flex items-center gap-1.5 text-xs opacity-60">
{isUser ? <UserIcon size={12} /> : <Bot size={12} />}
{isUser ? t("chat.you") : "KisAssistant"}
@@ -39,7 +39,7 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport, isFaile
<UserMessageContent content={msg.content} />
</div>
) : (
<div className="rounded-3xl rounded-tl-lg border border-stone-200/70 bg-white px-5 py-3.5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="rounded-3xl rounded-tl-lg border border-stone-200/70 bg-white px-4 py-3 shadow-sm sm:px-5 sm:py-3.5 dark:border-zinc-800 dark:bg-zinc-900">
<Markdown text={msg.content} />
</div>
)}
@@ -313,8 +313,8 @@ export function Chat({
</div>
</header>
<div ref={boxRef} className="flex-1 space-y-5 overflow-y-auto px-4 py-6 sm:px-8">
<div className="mx-auto w-full max-w-3xl space-y-5">
<div ref={boxRef} className="flex-1 space-y-4 overflow-y-auto px-4 py-4 sm:space-y-5 sm:px-8 sm:py-6">
<div className="mx-auto w-full max-w-3xl space-y-4 sm:space-y-5">
{messages.length === 0 && !streaming && (
<div className="py-16 text-center">
<FlowerMark size={56} dynamic={false} className="mx-auto mb-4" />
@@ -326,11 +326,11 @@ export function Chat({
<MessageBubble key={i} msg={m} index={i} authorAvatar={user.avatarUrl} authorName={user.displayName} onReport={readOnly ? undefined : setReportIndex} isFailed={failedIndex === i} retryCount={retryCount} maxRetries={maxRetries} retryDisabled={sending} onRetry={failedIndex === i && retryCount < maxRetries ? onRetry : undefined} />
))}
{streaming !== "" && (
<div className="flex gap-3">
<div className="flex gap-2 sm:gap-3">
<AssistantAvatar size={32} />
<div className="min-w-0 max-w-[85%]">
<div className="min-w-0 max-w-[88%] sm:max-w-[85%]">
<div className="mb-1 text-xs opacity-60">KisAssistant</div>
<div className="rounded-3xl rounded-tl-lg border border-stone-200/70 bg-white px-5 py-3.5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="rounded-3xl rounded-tl-lg border border-stone-200/70 bg-white px-4 py-3 shadow-sm sm:px-5 sm:py-3.5 dark:border-zinc-800 dark:bg-zinc-900">
<div className="typing-cursor"><Markdown text={streaming} /></div>
</div>
</div>
+9
View File
@@ -36,6 +36,15 @@ export function Markdown({ text }: { text: string }) {
code({ children }) {
return <code>{children}</code>;
},
table({ children }) {
// Wide tables would blow the bubble off-screen on mobile —
// scroll them inside their own pane instead.
return (
<div className="overflow-x-auto pb-1">
<table>{children}</table>
</div>
);
},
}}
>
{text}
+19 -2
View File
@@ -402,6 +402,11 @@ export function SettingsModal({
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
// The search field lives inside the settings <form>: Enter must
// only filter, never submit (save + close the modal).
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
placeholder={t("settings.searchPh")}
aria-label={t("settings.searchAria")}
className="w-full rounded-2xl border border-stone-200/70 bg-stone-100 py-2 pl-9 pr-8 text-sm outline-none transition placeholder:text-stone-400 focus:border-accent-500/60 focus:bg-white dark:border-zinc-800 dark:bg-zinc-800/60 dark:placeholder:text-zinc-500 dark:focus:bg-zinc-900"
@@ -419,7 +424,16 @@ export function SettingsModal({
</div>
{search.trim() && visibleCats.length === 0 ? (
<p className="px-2 py-4 text-center text-sm opacity-50">{t("settings.noMatch", { q: search.trim() })}</p>
<div className="space-y-2 px-2 py-4 text-center">
<p className="text-sm opacity-50">{t("settings.noMatch", { q: search.trim() })}</p>
<button
type="button"
onClick={() => setSearch("")}
className="rounded-full border border-stone-200 bg-white px-3.5 py-1.5 text-sm transition hover:bg-stone-50 dark:border-zinc-700 dark:bg-zinc-900 dark:hover:bg-zinc-800"
>
{t("settings.clearSearch")}
</button>
</div>
) : (
<div className="flex flex-col gap-4 sm:flex-row">
<nav aria-label="Settings categories" className="flex shrink-0 gap-1.5 overflow-x-auto sm:w-44 sm:flex-col">
@@ -427,7 +441,10 @@ export function SettingsModal({
<button
key={c.id}
type="button"
onClick={() => setCat(c.id)}
// Picking a category exits search mode: keeping the query
// would leave the nav filtered (often down to this single
// button, or empty) and trap the user in the search view.
onClick={() => { setCat(c.id); setSearch(""); }}
aria-current={activeCat === c.id}
className={cn(
"flex shrink-0 items-center gap-2.5 rounded-2xl px-3.5 py-2.5 text-sm transition",
+1 -1
View File
@@ -465,7 +465,7 @@ const GRADIENTS = [
export function Avatar({ name, url, size = 36 }: { name: string; url?: string; size?: number }) {
const cdnV = useCdnVersion();
if (url) {
return <img src={withCdnVersion(url, cdnV)} alt={name} width={size} height={size} className="rounded-full object-cover" style={{ width: size, height: size }} />;
return <img src={withCdnVersion(url, cdnV)} alt={name} width={size} height={size} className="shrink-0 rounded-full object-cover" style={{ width: size, height: size }} />;
}
const initial = (name.trim()[0] || "?").toUpperCase();
const g = GRADIENTS[(name.charCodeAt(0) || 0) % GRADIENTS.length];