From 1fe5acc871b4c20f34681f1ec7a7bdd5b44b6866 Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Wed, 29 Apr 2026 23:05:29 +0200 Subject: [PATCH] fix: ui chat --- README.md | 2 +- src/components/Sidebar.tsx | 2 +- src/components/chat/ChatUI.tsx | 102 ++++++++++++++++++--------- src/components/chat/stream.ts | 2 + src/components/icons/CancelIcon.tsx | 11 +++ src/components/icons/MessageIcon.tsx | 18 +++++ 6 files changed, 100 insertions(+), 37 deletions(-) create mode 100644 src/components/icons/CancelIcon.tsx create mode 100644 src/components/icons/MessageIcon.tsx diff --git a/README.md b/README.md index 80a4e78..b9d35b4 100644 --- a/README.md +++ b/README.md @@ -144,4 +144,4 @@ git tag -d v1.0.0; git push origin :refs/tags/v1.0.0; git tag -a v1.0.0 -m "Rele - Token refresh - No error when streaming with too large model - Translations -- stop models during geenration +- When user stop generation, forward it to server diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index ec7a0f4..ec0ac36 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -142,7 +142,7 @@ function IconRail({ onClick={() => toggle(panel.id, effectiveId)} title={panel.label} className={`w-8 h-8 rounded-lg flex items-center justify-center text-secondary ${ - effectiveId === panel.id ? 'border-2 border-[var(--accent2)]' : '' + effectiveId === panel.id ? 'border-2 border-[var(--accent)]' : '' }`} > {panel.icon} diff --git a/src/components/chat/ChatUI.tsx b/src/components/chat/ChatUI.tsx index 5bb8530..428c1fe 100644 --- a/src/components/chat/ChatUI.tsx +++ b/src/components/chat/ChatUI.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef } from 'react'; import ModelSelect from './ModelSelect'; import Sidebar from '../Sidebar'; import { SIDEBAR_PANELS } from './chat.panels'; @@ -9,6 +9,8 @@ import { Message, ModelInfo } from '@/types/chat-api'; import { ChatRequest } from '@/types/chat-api'; import { Settings } from './chat.panels'; import { fetchChatStream, readStreamChunks } from './stream'; +import { MessageIcon } from '../icons/MessageIcon'; +import { CancelIcon } from '../icons/CancelIcon'; type Props = { session: Session; @@ -123,36 +125,52 @@ export default function ChatUI({ // ── Streaming path ──────────────────────────────────────────────────────── + const abortRef = useRef(null); + + const stopStream = () => { + abortRef.current?.abort(); + }; + const handleStream = async ( endpoint: string, body: ChatRequest, priorMessages: Message[], ) => { - // Append an empty assistant message that we'll fill in as tokens arrive - const placeholderIndex = priorMessages.length; // index in the upcoming state + const placeholderIndex = priorMessages.length; setMessages((prev) => [...prev, { role: ASSISTANT_ROLE, content: '' }]); + const controller = new AbortController(); + abortRef.current = controller; + const response = await fetchChatStream( endpoint, body, session?.accessToken as string, + controller.signal, ); - for await (const { content, done } of readStreamChunks(response)) { - if (done) break; - if (!content) continue; + try { + for await (const { content, done } of readStreamChunks(response)) { + if (done) break; + if (!content) continue; - setMessages((prev) => { - const next = [...prev]; - const target = next[placeholderIndex]; - if (target) { - next[placeholderIndex] = { - ...target, - content: target.content + content, - }; - } - return next; - }); + setMessages((prev) => { + const next = [...prev]; + const target = next[placeholderIndex]; + if (target) { + next[placeholderIndex] = { + ...target, + content: target.content + content, + }; + } + return next; + }); + } + } catch (err: any) { + // AbortError is expected when the user clicks Stop — don't rethrow + if (err?.name !== 'AbortError') throw err; + } finally { + abortRef.current = null; } }; @@ -166,12 +184,9 @@ export default function ChatUI({ )} - {/* ── Sidebar (icon rail + sliding panel) ── */} - {/* ── Main chat area ── */}
- {/* Top bar */}
- {/* Messages + centered input when empty */}
{messages.length === 0 ? ( - /* ── Empty state: input centered within the chat column only ── */
-

- Start a conversation… -

+

Start a conversation…

setInput(e.target.value)} @@ -200,7 +211,6 @@ export default function ChatUI({ />
) : ( - /* ── Active chat: messages list ── */
{messages.map((msg, i) => (
@@ -220,17 +230,39 @@ export default function ChatUI({ )}
- {/* Input bar — only shown once conversation has started */} {messages.length > 0 && (
- setInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && !loading && sendMessage()} - disabled={loading} - className="w-full max-w-2xl mx-auto block p-3 rounded-xl bg-secondary border border-muted outline-none disabled:opacity-50" - placeholder="Message..." - /> +
+ setInput(e.target.value)} + onKeyDown={(e) => + e.key === 'Enter' && !loading && sendMessage() + } + className="w-full p-3 pr-12 rounded-xl bg-secondary border border-muted outline-none" + placeholder="Message..." + /> +
+ {loading ? ( + + ) : ( + + )} +
+
)}
diff --git a/src/components/chat/stream.ts b/src/components/chat/stream.ts index 7e1648d..5ace793 100644 --- a/src/components/chat/stream.ts +++ b/src/components/chat/stream.ts @@ -92,9 +92,11 @@ export async function fetchChatStream( url: string, body: object, token: string, + signal?: AbortSignal, ): Promise { const res = await fetch(url, { method: 'POST', + signal, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, diff --git a/src/components/icons/CancelIcon.tsx b/src/components/icons/CancelIcon.tsx new file mode 100644 index 0000000..833930b --- /dev/null +++ b/src/components/icons/CancelIcon.tsx @@ -0,0 +1,11 @@ +export const CancelIcon = (props: React.SVGProps) => ( + + + +); diff --git a/src/components/icons/MessageIcon.tsx b/src/components/icons/MessageIcon.tsx new file mode 100644 index 0000000..6c52ed3 --- /dev/null +++ b/src/components/icons/MessageIcon.tsx @@ -0,0 +1,18 @@ +export function MessageIcon(props: React.SVGProps) { + return ( + + + + + ); +}