improve ui consistency with immer; implement `addConversation`

master
Avraham Sakal 3 months ago
parent 7a9f0c956c
commit de26fc5eb5

@ -1,4 +1,5 @@
import "@mantine/core/styles.css";
import { navigate } from "vike/client/router";
import {
AppShell,
Burger,
@ -15,6 +16,7 @@ import {
IconCircle,
IconCircleFilled,
IconTrashFilled,
IconPlus,
} from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks";
import theme from "./theme.js";
@ -36,7 +38,7 @@ export default function LayoutDefault({
const setConversations = useStore((state) => state.setConversations);
const addConversation = useStore((state) => state.addConversation);
const removeConversation = useStore((state) => state.removeConversation);
const conversationId = useStore((state) => state.conversationId);
const conversationId = useStore((state) => state.selectedConversationId);
useEffect(() => {
trpc.chat.listConversations.query().then((res) => {
@ -96,11 +98,25 @@ export default function LayoutDefault({
label="Chats"
leftSection={<IconActivity size={16} stroke={1.5} />}
rightSection={
<IconChevronRight
size={12}
stroke={1.5}
className="mantine-rotate-rtl"
/>
<>
<IconPlus
size={16}
stroke={1.5}
className="border-on-hover"
onClick={() => {
trpc.chat.createConversation.mutate().then((res) => {
if (!res?.id) return;
addConversation(res);
navigate(`/chat/${res.id}`);
});
}}
/>
<IconChevronRight
size={12}
stroke={1.5}
className="mantine-rotate-rtl"
/>
</>
}
variant="subtle"
active={urlPathname.startsWith("/chat")}

@ -29,6 +29,7 @@
"ai": "^4.3.16",
"dotenv": "^17.0.0",
"hono": "^4.8.2",
"immer": "^10.1.1",
"kysely": "^0.28.2",
"pg": "^8.16.3",
"react": "^19.1.0",

@ -15,7 +15,9 @@ import type { ConversationsId } from "../../../database/generated/public/Convers
export default function ChatPage() {
const pageContext = usePageContext();
const conversationId = Number(pageContext.routeParams.id) as ConversationsId;
const conversationTitle = useStore((state) => state.conversationTitle);
const conversationTitle = useStore(
(state) => state.conversations.find((c) => c.id === conversationId)?.title,
);
const messages = useStore((state) => state.messages);
const message = useStore((state) => state.message);
const systemPrompt = useStore((state) => state.systemPrompt);
@ -53,7 +55,7 @@ export default function ChatPage() {
<span>Conversation #{conversationId} - </span>
<input
type="text"
value={conversationTitle}
value={conversationTitle || ""}
onChange={(e) => {
setConversationTitle(e.target.value);
}}
@ -88,7 +90,7 @@ export default function ChatPage() {
];
setMessages(messagesWithNewUserMessage);
setLoading(true);
const response = await trpc.chat.sendMessage.query({
const response = await trpc.chat.sendMessage.mutate({
messages: messagesWithNewUserMessage,
systemPrompt,
parameters,

@ -84,7 +84,7 @@ export const chat = router({
parameters: OtherParameters;
},
)
.query(async ({ input: { messages, systemPrompt, parameters } }) => {
.mutation(async ({ input: { messages, systemPrompt, parameters } }) => {
const response = await generateText({
model: openrouter("mistralai/mistral-nemo"),
messages: [

@ -62,6 +62,9 @@ importers:
hono:
specifier: ^4.8.2
version: 4.8.3
immer:
specifier: ^10.1.1
version: 10.1.1
kysely:
specifier: ^0.28.2
version: 0.28.2
@ -88,7 +91,7 @@ importers:
version: 3.25.67
zustand:
specifier: ^5.0.6
version: 5.0.6(@types/react@19.1.8)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
version: 5.0.6(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
devDependencies:
'@biomejs/biome':
specifier: 1.9.4
@ -1652,6 +1655,9 @@ packages:
resolution: {integrity: sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ==}
engines: {node: '>=16.9.0'}
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@ -4202,6 +4208,8 @@ snapshots:
hono@4.8.3: {}
immer@10.1.1: {}
inherits@2.0.4: {}
interpret@2.2.0: {}
@ -5320,8 +5328,9 @@ snapshots:
zod@3.25.67: {}
zustand@5.0.6(@types/react@19.1.8)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)):
zustand@5.0.6(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)):
optionalDependencies:
'@types/react': 19.1.8
immer: 10.1.1
react: 19.1.0
use-sync-external-store: 1.5.0(react@19.1.0)

@ -1,6 +1,7 @@
import { create } from "zustand";
import type { OtherParameters, Store } from "./types.js";
import type { ConversationsId } from "./database/generated/public/Conversations.js";
import { immer } from "zustand/middleware/immer";
export const defaultSystemPrompt = `You are a helpful assistant that answers questions based on the provided context. If you don't know the answer, just say that you don't know, don't try to make up an answer.`;
export const defaultParameters = {
@ -8,31 +9,65 @@ export const defaultParameters = {
max_tokens: 100,
} as OtherParameters;
export const useStore = create<Store>()((set) => ({
conversationId: 0 as ConversationsId,
conversationTitle: "",
conversations: [],
messages: [],
message: "",
systemPrompt: defaultSystemPrompt,
parameters: defaultParameters,
loading: false,
setConversationId: (conversationId) => set({ conversationId }),
setConversationTitle: (conversationTitle) => set({ conversationTitle }),
setConversations: (conversations) => set({ conversations }),
addConversation: (conversation) =>
set((state) => ({
conversations: [...state.conversations, conversation],
})),
removeConversation: (conversationId) =>
set((state) => ({
conversations: state.conversations.filter(
(conversation) => conversation.id !== conversationId,
),
})),
setMessages: (messages) => set({ messages }),
setMessage: (message) => set({ message }),
setSystemPrompt: (systemPrompt) => set({ systemPrompt }),
setParameters: (parameters) => set({ parameters }),
setLoading: (loading) => set({ loading }),
}));
export const useStore = create<Store>()(
immer((set, get) => ({
selectedConversationId: 0 as ConversationsId,
conversations: [],
messages: [],
message: "",
systemPrompt: defaultSystemPrompt,
parameters: defaultParameters,
loading: false,
setConversationId: (conversationId) =>
set((stateDraft) => {
stateDraft.selectedConversationId = conversationId;
}),
setConversationTitle: (conversationTitle) =>
set((stateDraft) => {
const conversation = stateDraft.conversations.find(
(c) => c.id === stateDraft.selectedConversationId,
);
if (conversation) {
conversation.title = conversationTitle;
}
}),
setConversations: (conversations) =>
set((stateDraft) => {
stateDraft.conversations = conversations;
}),
addConversation: (conversation) =>
set((stateDraft) => {
stateDraft.conversations.push(conversation);
}),
removeConversation: (conversationId) =>
set((stateDraft) => {
stateDraft.conversations.splice(
stateDraft.conversations.findIndex(
(conversation) => conversation.id === conversationId,
),
1,
);
}),
setMessages: (messages) =>
set((stateDraft) => {
//@ts-ignore
stateDraft.messages = messages;
}),
setMessage: (message) =>
set((stateDraft) => {
stateDraft.message = message;
}),
setSystemPrompt: (systemPrompt) =>
set((stateDraft) => {
stateDraft.systemPrompt = systemPrompt;
}),
setParameters: (parameters) =>
set((stateDraft) => {
stateDraft.parameters = parameters;
}),
setLoading: (loading) =>
set((stateDraft) => {
stateDraft.loading = loading;
}),
})),
);

@ -15,8 +15,7 @@ export type ConversationUI = Conversations & {};
export type Store = {
/** This is a string because Milvus sends it as a string, and the value
* overflows the JS integer anyway. */
conversationId: ConversationsId;
conversationTitle: string;
selectedConversationId: ConversationsId;
conversations: Array<ConversationUI>;
messages: Array<UIMessage>;
message: string;

Loading…
Cancel
Save