- applications/iknowyou: new Next.js chat application with persona-aware conversations, briefing API, cohort logic, vLLM streaming, and sidebar navigation - tidal M8: add replication control plane (control.rs), tenant migration state machine (migration.rs), tenant/upgrade coordinators, cluster/fault test harnesses - tidal M8 tests: expand m8p2/m8p3/m8p4 test suites; add m8p5_multitenancy and m8_uat - tidal db: split replication_ops out of db/mod.rs (was 647 lines, now 574) - .claude: add kai-park, kaya-osei, mira-vasquez agents; add aeries-design-architect, aeries-fullstack-engineer, aeries-product-visionary skills - docs: update ROADMAP.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
192 lines
4.8 KiB
TypeScript
192 lines
4.8 KiB
TypeScript
import { create } from "zustand";
|
|
import { persist } from "zustand/middleware";
|
|
import type { ChatState, Conversation } from "./types";
|
|
|
|
function genId(): string {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
export const useChatStore = create<ChatState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
messages: [],
|
|
isStreaming: false,
|
|
error: null,
|
|
|
|
conversations: [],
|
|
activeConversationId: null,
|
|
personId: crypto.randomUUID(),
|
|
|
|
// --- Messages ---
|
|
|
|
addUserMessage: (content: string) => {
|
|
const id = genId();
|
|
const now = Date.now();
|
|
set((s) => {
|
|
const updated = {
|
|
messages: [
|
|
...s.messages,
|
|
{ id, role: "user" as const, content, timestamp: now },
|
|
],
|
|
error: null,
|
|
};
|
|
|
|
// Update conversation title from first user message
|
|
if (s.activeConversationId) {
|
|
const conv = s.conversations.find(
|
|
(c) => c.id === s.activeConversationId
|
|
);
|
|
if (conv && conv.title === "New conversation") {
|
|
return {
|
|
...updated,
|
|
conversations: s.conversations.map((c) =>
|
|
c.id === s.activeConversationId
|
|
? {
|
|
...c,
|
|
title: content.slice(0, 60),
|
|
lastMessageAt: now,
|
|
}
|
|
: c
|
|
),
|
|
};
|
|
}
|
|
return {
|
|
...updated,
|
|
conversations: s.conversations.map((c) =>
|
|
c.id === s.activeConversationId
|
|
? { ...c, lastMessageAt: now }
|
|
: c
|
|
),
|
|
};
|
|
}
|
|
|
|
return updated;
|
|
});
|
|
return id;
|
|
},
|
|
|
|
startStreaming: () => {
|
|
const id = genId();
|
|
set((s) => ({
|
|
messages: [
|
|
...s.messages,
|
|
{
|
|
id,
|
|
role: "assistant" as const,
|
|
content: "",
|
|
timestamp: Date.now(),
|
|
},
|
|
],
|
|
isStreaming: true,
|
|
error: null,
|
|
}));
|
|
return id;
|
|
},
|
|
|
|
appendToken: (id: string, token: string) => {
|
|
set((s) => ({
|
|
messages: s.messages.map((m) =>
|
|
m.id === id ? { ...m, content: m.content + token } : m
|
|
),
|
|
}));
|
|
},
|
|
|
|
finishStreaming: (id: string) => {
|
|
set((s) => ({
|
|
messages: s.messages.map((m) =>
|
|
m.id === id ? { ...m, timestamp: Date.now() } : m
|
|
),
|
|
isStreaming: false,
|
|
}));
|
|
},
|
|
|
|
setError: (error: string | null) => {
|
|
set({ error, isStreaming: false });
|
|
},
|
|
|
|
clearMessages: () => {
|
|
set({ messages: [], error: null, isStreaming: false });
|
|
},
|
|
|
|
// --- Conversations ---
|
|
|
|
createConversation: () => {
|
|
const id = genId();
|
|
const now = Date.now();
|
|
const conv: Conversation = {
|
|
id,
|
|
title: "New conversation",
|
|
createdAt: now,
|
|
lastMessageAt: now,
|
|
};
|
|
set((s) => ({
|
|
conversations: [conv, ...s.conversations],
|
|
activeConversationId: id,
|
|
messages: [],
|
|
error: null,
|
|
isStreaming: false,
|
|
}));
|
|
return id;
|
|
},
|
|
|
|
switchConversation: (id: string) => {
|
|
const { activeConversationId } = get();
|
|
if (id === activeConversationId) return;
|
|
set({
|
|
activeConversationId: id,
|
|
messages: [],
|
|
error: null,
|
|
isStreaming: false,
|
|
});
|
|
},
|
|
|
|
setMessages: (msgs) => {
|
|
set({ messages: msgs });
|
|
},
|
|
|
|
updateConversationTitle: (id: string, title: string) => {
|
|
set((s) => ({
|
|
conversations: s.conversations.map((c) =>
|
|
c.id === id ? { ...c, title: title.slice(0, 60) } : c
|
|
),
|
|
}));
|
|
},
|
|
|
|
deleteConversation: (id: string) => {
|
|
set((s) => {
|
|
const filtered = s.conversations.filter((c) => c.id !== id);
|
|
const wasActive = s.activeConversationId === id;
|
|
return {
|
|
conversations: filtered,
|
|
activeConversationId: wasActive
|
|
? filtered[0]?.id ?? null
|
|
: s.activeConversationId,
|
|
messages: wasActive ? [] : s.messages,
|
|
};
|
|
});
|
|
},
|
|
|
|
switchPerson: () => {
|
|
const newId = genId();
|
|
set({
|
|
personId: newId,
|
|
conversations: [],
|
|
activeConversationId: null,
|
|
messages: [],
|
|
error: null,
|
|
isStreaming: false,
|
|
});
|
|
return newId;
|
|
},
|
|
}),
|
|
{
|
|
name: "aeries-conversations",
|
|
partialize: (state) => ({
|
|
conversations: state.conversations,
|
|
activeConversationId: state.activeConversationId,
|
|
personId: state.personId,
|
|
}),
|
|
}
|
|
)
|
|
);
|