Skip to content

Commit e5123ce

Browse files
prplxprplx
authored andcommitted
feat(ui): improve chat motion and simplify writing simple background
1 parent e679ad6 commit e5123ce

3 files changed

Lines changed: 140 additions & 65 deletions

File tree

src/features/chat/ChatScreen.tsx

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ interface ParsedToolCallContent {
127127
}
128128

129129
const REASONING_CALL_NAME = "__reasoning__";
130+
const MESSAGE_DELETE_ANIMATION_MS = 180;
130131

131132
interface StreamingToolCall {
132133
callId: string;
@@ -186,6 +187,7 @@ export function ChatScreen() {
186187
const [editingId, setEditingId] = useState<string | null>(null);
187188
const [editingValue, setEditingValue] = useState("");
188189
const [streamText, setStreamText] = useState("");
190+
const [streamChunks, setStreamChunks] = useState<Array<{ id: number; text: string }>>([]);
189191
const [streaming, setStreaming] = useState(false);
190192
const [streamingCharacterName, setStreamingCharacterName] = useState<string | null>(null);
191193
const [streamingToolCalls, setStreamingToolCalls] = useState<StreamingToolCall[]>([]);
@@ -288,12 +290,14 @@ export function ChatScreen() {
288290
});
289291
const [toolPanelsExpanded, setToolPanelsExpanded] = useState<Record<string, boolean>>({});
290292
const [reasoningPanelsExpanded, setReasoningPanelsExpanded] = useState<Record<string, boolean>>({});
293+
const [deletingMessageIds, setDeletingMessageIds] = useState<Record<string, boolean>>({});
291294

292295
const messagesEndRef = useRef<HTMLDivElement>(null);
293296
const textareaRef = useRef<HTMLTextAreaElement>(null);
294297
const chatSearchInputRef = useRef<HTMLInputElement>(null);
295298
const modelSelectorRef = useRef<HTMLDivElement>(null);
296299
const modelSelectorTriggerRef = useRef<HTMLButtonElement>(null);
300+
const streamChunkIdRef = useRef(0);
297301

298302
const orderedBlocks = useMemo(
299303
() => normalizePromptStack(promptStack),
@@ -413,6 +417,23 @@ export function ChatScreen() {
413417
return () => window.removeEventListener("keydown", onKeyDown);
414418
}, [zenMode]);
415419

420+
useEffect(() => {
421+
setDeletingMessageIds((prev) => {
422+
const liveIds = new Set(messages.map((msg) => msg.id));
423+
let changed = false;
424+
const next: Record<string, boolean> = {};
425+
for (const [id, value] of Object.entries(prev)) {
426+
if (!value) continue;
427+
if (liveIds.has(id)) {
428+
next[id] = true;
429+
} else {
430+
changed = true;
431+
}
432+
}
433+
return changed ? next : prev;
434+
});
435+
}, [messages]);
436+
416437
useEffect(() => {
417438
if (!simpleModeActive) return;
418439
const onKeyDown = (event: KeyboardEvent) => {
@@ -714,6 +735,8 @@ export function ChatScreen() {
714735

715736
function startStreamingUi(characterName: string | null) {
716737
setStreamText("");
738+
setStreamChunks([]);
739+
streamChunkIdRef.current = 0;
717740
setStreaming(true);
718741
setStreamingCharacterName(characterName);
719742
setStreamingToolCalls([]);
@@ -725,6 +748,8 @@ export function ChatScreen() {
725748
function stopStreamingUi() {
726749
setStreaming(false);
727750
setStreamText("");
751+
setStreamChunks([]);
752+
streamChunkIdRef.current = 0;
728753
setStreamingCharacterName(null);
729754
setStreamingToolCalls([]);
730755
setStreamingReasoningCalls([]);
@@ -770,6 +795,12 @@ export function ChatScreen() {
770795
});
771796
}
772797

798+
const appendStreamDelta = useCallback((delta: string) => {
799+
if (!delta) return;
800+
setStreamText((prev) => prev + delta);
801+
setStreamChunks((prev) => [...prev, { id: ++streamChunkIdRef.current, text: delta }]);
802+
}, []);
803+
773804
const savePromptStack = useCallback(
774805
(newBlocks: PromptBlock[]) => {
775806
const normalized = normalizePromptStack(newBlocks);
@@ -886,7 +917,7 @@ export function ChatScreen() {
886917
startStreamingUi(null);
887918

888919
const updated = await api.chatSend(chatId, outgoing, branchId || undefined, {
889-
onDelta: (delta) => setStreamText((prev) => prev + delta),
920+
onDelta: appendStreamDelta,
890921
onToolEvent: handleStreamingToolEvent,
891922
onDone: () => { stopStreamingUi(); }
892923
}, activePersonaPayload, currentAttachments);
@@ -921,7 +952,7 @@ export function ChatScreen() {
921952
return prev.filter((msg) => msg.id !== lastAssistant.id && msg.parentId !== lastAssistant.id);
922953
});
923954
const updated = await api.chatRegenerate(activeChat.id, activeBranchId || undefined, {
924-
onDelta: (delta) => setStreamText((prev) => prev + delta),
955+
onDelta: appendStreamDelta,
925956
onToolEvent: handleStreamingToolEvent,
926957
onDone: () => { stopStreamingUi(); }
927958
});
@@ -1051,8 +1082,22 @@ export function ChatScreen() {
10511082
}
10521083

10531084
async function handleDelete(messageId: string) {
1054-
const result = await api.chatDeleteMessage(messageId);
1055-
setMessages(result.timeline);
1085+
if (deletingMessageIds[messageId]) return;
1086+
setDeletingMessageIds((prev) => ({ ...prev, [messageId]: true }));
1087+
try {
1088+
await new Promise((resolve) => setTimeout(resolve, MESSAGE_DELETE_ANIMATION_MS));
1089+
const result = await api.chatDeleteMessage(messageId);
1090+
setMessages(result.timeline);
1091+
} catch (error) {
1092+
setErrorText(String(error));
1093+
} finally {
1094+
setDeletingMessageIds((prev) => {
1095+
if (!prev[messageId]) return prev;
1096+
const next = { ...prev };
1097+
delete next[messageId];
1098+
return next;
1099+
});
1100+
}
10561101
}
10571102

10581103
async function saveEdit(messageId: string) {
@@ -1220,7 +1265,7 @@ export function ChatScreen() {
12201265
startStreamingUi(characterName);
12211266
try {
12221267
const updated = await api.chatNextTurn(activeChat.id, characterName, activeBranchId || undefined, {
1223-
onDelta: (delta) => setStreamText((prev) => prev + delta),
1268+
onDelta: appendStreamDelta,
12241269
onToolEvent: handleStreamingToolEvent,
12251270
onDone: () => { stopStreamingUi(); }
12261271
}, false, activePersonaPayload);
@@ -1264,7 +1309,7 @@ export function ChatScreen() {
12641309

12651310
try {
12661311
const updated = await api.chatNextTurn(activeChat.id, charName, activeBranchId || undefined, {
1267-
onDelta: (delta) => setStreamText((prev) => prev + delta),
1312+
onDelta: appendStreamDelta,
12681313
onToolEvent: handleStreamingToolEvent,
12691314
onDone: () => { stopStreamingUi(); }
12701315
}, true, activePersonaPayload); // isAutoConvo = true
@@ -1843,20 +1888,21 @@ export function ChatScreen() {
18431888
)}
18441889

18451890
{!simpleSidebarCollapsed && (
1846-
<div className="flex-1 space-y-1 overflow-y-auto">
1891+
<div className="chat-sidebar-list flex-1 space-y-1 overflow-y-auto">
18471892
{chats.length === 0 ? (
18481893
<EmptyState title={t("chat.noChatYet")} description={t("chat.noChatDesc")} />
18491894
) : filteredChats.length === 0 ? (
18501895
<EmptyState title={t("chat.noSearchResults")} description={t("chat.noSearchResultsDesc")} />
18511896
) : (
1852-
filteredChats.map((chat) => {
1897+
filteredChats.map((chat, index) => {
18531898
const primaryChatCharacterId = chat.characterId || chat.characterIds?.[0] || null;
18541899
const chatChar = primaryChatCharacterId ? characters.find((c) => c.id === primaryChatCharacterId) : null;
18551900
const multiCount = chat.characterIds?.length || 0;
18561901
const isRenaming = renamingChatId === chat.id;
18571902
return (
18581903
<div key={chat.id}
1859-
className={`group relative flex items-start gap-2 rounded-lg ${simpleModeActive ? "px-2 py-2" : "px-3 py-2"} transition-colors ${
1904+
style={{ animationDelay: `${Math.min(index, 20) * 20}ms` }}
1905+
className={`chat-sidebar-item group relative flex items-start gap-2 rounded-lg ${simpleModeActive ? "px-2 py-2" : "px-3 py-2"} transition-colors ${
18601906
activeChat?.id === chat.id ? "bg-accent-subtle text-text-primary" : "text-text-secondary hover:bg-bg-hover"
18611907
}`}>
18621908
{isRenaming ? (
@@ -2355,7 +2401,7 @@ export function ChatScreen() {
23552401
const renderCharName = msgChar?.name || activeChatCharacter?.name;
23562402
return (
23572403
<article key={msg.id}
2358-
className={`chat-message group max-w-[88%] rounded-xl px-3 py-2 text-sm leading-relaxed ${
2404+
className={`chat-message group max-w-[88%] rounded-xl px-3 py-2 text-sm leading-relaxed ${deletingMessageIds[msg.id] ? "is-deleting" : ""} ${
23592405
msg.role === "user"
23602406
? "chat-message-user ml-auto bg-accent-subtle text-text-primary"
23612407
: "chat-message-assistant mr-auto border border-border-subtle bg-bg-secondary text-text-primary"
@@ -2546,7 +2592,8 @@ export function ChatScreen() {
25462592
</button>
25472593
)}
25482594
<button onClick={() => handleDelete(msg.id)}
2549-
className="rounded-md px-2 py-0.5 text-[11px] text-danger/60 hover:bg-danger-subtle hover:text-danger">{t("chat.delete")}</button>
2595+
disabled={deletingMessageIds[msg.id]}
2596+
className="rounded-md px-2 py-0.5 text-[11px] text-danger/60 hover:bg-danger-subtle hover:text-danger disabled:opacity-40">{t("chat.delete")}</button>
25502597
</div>
25512598
)}
25522599
</article>
@@ -2596,9 +2643,13 @@ export function ChatScreen() {
25962643
)}
25972644
</div>
25982645
)}
2599-
<div className="prose-chat" dangerouslySetInnerHTML={{
2600-
__html: streamText ? renderContent(streamText, streamChar?.name, activePersona?.name || t("chat.user")) : "..."
2601-
}} />
2646+
<div className="chat-stream-content chat-stream-live">
2647+
{streamChunks.length > 0
2648+
? streamChunks.map((chunk) => (
2649+
<span key={chunk.id} className="chat-stream-chunk">{chunk.text}</span>
2650+
))
2651+
: (streamText ? streamText : "...")}
2652+
</div>
26022653
{!zenMode && streamingToolCalls.length > 0 && (
26032654
<div className="mt-2 rounded-md border border-warning-border bg-warning-subtle">
26042655
<button

src/features/writer/WritingScreen.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,13 +1604,6 @@ export function WritingScreen() {
16041604
}
16051605
center={
16061606
<div className="flex h-full min-h-0 flex-col gap-3 overflow-y-auto pr-0.5">
1607-
{writingSimpleModeActive && (
1608-
<div className="writing-simple-ambient" aria-hidden="true">
1609-
<span className="writing-simple-blob blob-a" />
1610-
<span className="writing-simple-blob blob-b" />
1611-
<span className="writing-simple-blob blob-c" />
1612-
</div>
1613-
)}
16141607
{writingSimpleModeActive && (
16151608
<div className="writing-simple-top-controls">
16161609
<button

0 commit comments

Comments
 (0)