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"); window.open(`https://duckduckgo.com/?q=${encodeURIComponent(msg.content.slice(0, 400))}`, "_blank", "noopener,noreferrer");
} }
return ( 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 {isUser
? <Avatar name={authorName} url={authorAvatar} size={32} /> ? <Avatar name={authorName} url={authorAvatar} size={32} />
: <AssistantAvatar 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"> <div className="mb-1 flex items-center gap-1.5 text-xs opacity-60">
{isUser ? <UserIcon size={12} /> : <Bot size={12} />} {isUser ? <UserIcon size={12} /> : <Bot size={12} />}
{isUser ? t("chat.you") : "KisAssistant"} {isUser ? t("chat.you") : "KisAssistant"}
@@ -39,7 +39,7 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport, isFaile
<UserMessageContent content={msg.content} /> <UserMessageContent content={msg.content} />
</div> </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} /> <Markdown text={msg.content} />
</div> </div>
)} )}
@@ -313,8 +313,8 @@ export function Chat({
</div> </div>
</header> </header>
<div ref={boxRef} className="flex-1 space-y-5 overflow-y-auto px-4 py-6 sm:px-8"> <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-5"> <div className="mx-auto w-full max-w-3xl space-y-4 sm:space-y-5">
{messages.length === 0 && !streaming && ( {messages.length === 0 && !streaming && (
<div className="py-16 text-center"> <div className="py-16 text-center">
<FlowerMark size={56} dynamic={false} className="mx-auto mb-4" /> <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} /> <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 !== "" && ( {streaming !== "" && (
<div className="flex gap-3"> <div className="flex gap-2 sm:gap-3">
<AssistantAvatar size={32} /> <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="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 className="typing-cursor"><Markdown text={streaming} /></div>
</div> </div>
</div> </div>
+9
View File
@@ -36,6 +36,15 @@ export function Markdown({ text }: { text: string }) {
code({ children }) { code({ children }) {
return <code>{children}</code>; 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} {text}
+19 -2
View File
@@ -402,6 +402,11 @@ export function SettingsModal({
<input <input
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} 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")} placeholder={t("settings.searchPh")}
aria-label={t("settings.searchAria")} 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" 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> </div>
{search.trim() && visibleCats.length === 0 ? ( {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"> <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"> <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 <button
key={c.id} key={c.id}
type="button" 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} aria-current={activeCat === c.id}
className={cn( className={cn(
"flex shrink-0 items-center gap-2.5 rounded-2xl px-3.5 py-2.5 text-sm transition", "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 }) { export function Avatar({ name, url, size = 36 }: { name: string; url?: string; size?: number }) {
const cdnV = useCdnVersion(); const cdnV = useCdnVersion();
if (url) { 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 initial = (name.trim()[0] || "?").toUpperCase();
const g = GRADIENTS[(name.charCodeAt(0) || 0) % GRADIENTS.length]; const g = GRADIENTS[(name.charCodeAt(0) || 0) % GRADIENTS.length];