Browse Source

Merge pull request #2314 from Yidadaa/bugfix-0709-2

feat: #920  migrate id to nanoid
Yifei Zhang 1 year ago
parent
commit
332f3fb8e8

+ 9 - 9
app/client/controller.ts

@@ -3,17 +3,17 @@ export const ChatControllerPool = {
   controllers: {} as Record<string, AbortController>,
 
   addController(
-    sessionIndex: number,
-    messageId: number,
+    sessionId: string,
+    messageId: string,
     controller: AbortController,
   ) {
-    const key = this.key(sessionIndex, messageId);
+    const key = this.key(sessionId, messageId);
     this.controllers[key] = controller;
     return key;
   },
 
-  stop(sessionIndex: number, messageId: number) {
-    const key = this.key(sessionIndex, messageId);
+  stop(sessionId: string, messageId: string) {
+    const key = this.key(sessionId, messageId);
     const controller = this.controllers[key];
     controller?.abort();
   },
@@ -26,12 +26,12 @@ export const ChatControllerPool = {
     return Object.values(this.controllers).length > 0;
   },
 
-  remove(sessionIndex: number, messageId: number) {
-    const key = this.key(sessionIndex, messageId);
+  remove(sessionId: string, messageId: string) {
+    const key = this.key(sessionId, messageId);
     delete this.controllers[key];
   },
 
-  key(sessionIndex: number, messageIndex: number) {
-    return `${sessionIndex},${messageIndex}`;
+  key(sessionId: string, messageIndex: string) {
+    return `${sessionId},${messageIndex}`;
   },
 };

+ 1 - 1
app/components/chat-list.tsx

@@ -26,7 +26,7 @@ export function ChatItem(props: {
   count: number;
   time: string;
   selected: boolean;
-  id: number;
+  id: string;
   index: number;
   narrow?: boolean;
   mask: Mask;

+ 11 - 9
app/components/chat.tsx

@@ -221,9 +221,11 @@ function useSubmitHandler() {
   };
 }
 
+export type RenderPompt = Pick<Prompt, "title" | "content">;
+
 export function PromptHints(props: {
-  prompts: Prompt[];
-  onPromptSelect: (prompt: Prompt) => void;
+  prompts: RenderPompt[];
+  onPromptSelect: (prompt: RenderPompt) => void;
 }) {
   const noPrompts = props.prompts.length === 0;
   const [selectIndex, setSelectIndex] = useState(0);
@@ -542,7 +544,7 @@ export function Chat() {
 
   // prompt hints
   const promptStore = usePromptStore();
-  const [promptHints, setPromptHints] = useState<Prompt[]>([]);
+  const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
   const onSearch = useDebouncedCallback(
     (text: string) => {
       const matchedPrompts = promptStore.search(text);
@@ -624,7 +626,7 @@ export function Chat() {
     setAutoScroll(true);
   };
 
-  const onPromptSelect = (prompt: Prompt) => {
+  const onPromptSelect = (prompt: RenderPompt) => {
     setTimeout(() => {
       setPromptHints([]);
 
@@ -642,8 +644,8 @@ export function Chat() {
   };
 
   // stop response
-  const onUserStop = (messageId: number) => {
-    ChatControllerPool.stop(sessionIndex, messageId);
+  const onUserStop = (messageId: string) => {
+    ChatControllerPool.stop(session.id, messageId);
   };
 
   useEffect(() => {
@@ -703,7 +705,7 @@ export function Chat() {
     }
   };
 
-  const findLastUserIndex = (messageId: number) => {
+  const findLastUserIndex = (messageId: string) => {
     // find last user input message and resend
     let lastUserMessageIndex: number | null = null;
     for (let i = 0; i < session.messages.length; i += 1) {
@@ -719,14 +721,14 @@ export function Chat() {
     return lastUserMessageIndex;
   };
 
-  const deleteMessage = (msgId?: number) => {
+  const deleteMessage = (msgId?: string) => {
     chatStore.updateCurrentSession(
       (session) =>
         (session.messages = session.messages.filter((m) => m.id !== msgId)),
     );
   };
 
-  const onDelete = (msgId: number) => {
+  const onDelete = (msgId: string) => {
     deleteMessage(msgId);
   };
 

+ 3 - 3
app/components/exporter.tsx

@@ -8,7 +8,6 @@ import {
   Modal,
   Select,
   showImageModal,
-  showModal,
   showToast,
 } from "./ui-lib";
 import { IconButton } from "./button";
@@ -149,7 +148,7 @@ export function MessageExporter() {
     if (exportConfig.includeContext) {
       ret.push(...session.mask.context);
     }
-    ret.push(...session.messages.filter((m, i) => selection.has(m.id ?? i)));
+    ret.push(...session.messages.filter((m, i) => selection.has(m.id)));
     return ret;
   }, [
     exportConfig.includeContext,
@@ -244,9 +243,10 @@ export function RenderExport(props: {
       return;
     }
 
-    const renderMsgs = messages.map((v) => {
+    const renderMsgs = messages.map((v, i) => {
       const [_, role] = v.id.split(":");
       return {
+        id: i.toString(),
         role: role as any,
         content: v.innerHTML,
         date: "",

+ 16 - 7
app/components/mask.tsx

@@ -13,7 +13,13 @@ import EyeIcon from "../icons/eye.svg";
 import CopyIcon from "../icons/copy.svg";
 
 import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
-import { ChatMessage, ModelConfig, useAppConfig, useChatStore } from "../store";
+import {
+  ChatMessage,
+  createMessage,
+  ModelConfig,
+  useAppConfig,
+  useChatStore,
+} from "../store";
 import { ROLES } from "../client/api";
 import {
   Input,
@@ -35,6 +41,7 @@ import { Updater } from "../typing";
 import { ModelConfigList } from "./model-config";
 import { FileName, Path } from "../constant";
 import { BUILTIN_MASK_STORE } from "../masks";
+import { nanoid } from "nanoid";
 
 export function MaskAvatar(props: { mask: Mask }) {
   return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
@@ -279,11 +286,13 @@ export function ContextPrompts(props: {
             bordered
             className={chatStyle["context-prompt-button"]}
             onClick={() =>
-              addContextPrompt({
-                role: "user",
-                content: "",
-                date: "",
-              })
+              addContextPrompt(
+                createMessage({
+                  role: "user",
+                  content: "",
+                  date: "",
+                }),
+              )
             }
           />
         </div>
@@ -319,7 +328,7 @@ export function MaskPage() {
     }
   };
 
-  const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
+  const [editingMaskId, setEditingMaskId] = useState<string | undefined>();
   const editingMask =
     maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
   const closeMaskModal = () => setEditingMaskId(undefined);

+ 8 - 8
app/components/message-selector.tsx

@@ -51,9 +51,9 @@ function useShiftRange() {
 }
 
 export function useMessageSelector() {
-  const [selection, setSelection] = useState(new Set<number>());
-  const updateSelection: Updater<Set<number>> = (updater) => {
-    const newSelection = new Set<number>(selection);
+  const [selection, setSelection] = useState(new Set<string>());
+  const updateSelection: Updater<Set<string>> = (updater) => {
+    const newSelection = new Set<string>(selection);
     updater(newSelection);
     setSelection(newSelection);
   };
@@ -65,8 +65,8 @@ export function useMessageSelector() {
 }
 
 export function MessageSelector(props: {
-  selection: Set<number>;
-  updateSelection: Updater<Set<number>>;
+  selection: Set<string>;
+  updateSelection: Updater<Set<string>>;
   defaultSelectAll?: boolean;
   onSelected?: (messages: ChatMessage[]) => void;
 }) {
@@ -83,12 +83,12 @@ export function MessageSelector(props: {
   const config = useAppConfig();
 
   const [searchInput, setSearchInput] = useState("");
-  const [searchIds, setSearchIds] = useState(new Set<number>());
-  const isInSearchResult = (id: number) => {
+  const [searchIds, setSearchIds] = useState(new Set<string>());
+  const isInSearchResult = (id: string) => {
     return searchInput.length === 0 || searchIds.has(id);
   };
   const doSearch = (text: string) => {
-    const searchResults = new Set<number>();
+    const searchResults = new Set<string>();
     if (text.length > 0) {
       messages.forEach((m) =>
         m.content.includes(text) ? searchResults.add(m.id!) : null,

+ 1 - 2
app/components/new-chat.tsx

@@ -103,8 +103,7 @@ export function NewChat() {
   useCommand({
     mask: (id) => {
       try {
-        const intId = parseInt(id);
-        const mask = maskStore.get(intId) ?? BUILTIN_MASK_STORE.get(intId);
+        const mask = maskStore.get(id) ?? BUILTIN_MASK_STORE.get(id);
         startChat(mask ?? undefined);
       } catch {
         console.error("[New Chat] failed to create chat from mask id=", id);

+ 5 - 2
app/components/settings.tsx

@@ -48,8 +48,9 @@ import { useNavigate } from "react-router-dom";
 import { Avatar, AvatarPicker } from "./emoji";
 import { getClientConfig } from "../config/client";
 import { useSyncStore } from "../store/sync";
+import { nanoid } from "nanoid";
 
-function EditPromptModal(props: { id: number; onClose: () => void }) {
+function EditPromptModal(props: { id: string; onClose: () => void }) {
   const promptStore = usePromptStore();
   const prompt = promptStore.get(props.id);
 
@@ -107,7 +108,7 @@ function UserPromptModal(props: { onClose?: () => void }) {
   const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
   const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
 
-  const [editingPromptId, setEditingPromptId] = useState<number>();
+  const [editingPromptId, setEditingPromptId] = useState<string>();
 
   useEffect(() => {
     if (searchInput.length > 0) {
@@ -128,6 +129,8 @@ function UserPromptModal(props: { onClose?: () => void }) {
             key="add"
             onClick={() =>
               promptStore.add({
+                id: nanoid(),
+                createdAt: Date.now(),
                 title: "Empty Prompt",
                 content: "Empty Prompt Content",
               })

File diff suppressed because it is too large
+ 43 - 1
app/masks/cn.ts


File diff suppressed because it is too large
+ 1 - 0
app/masks/en.ts


+ 2 - 2
app/masks/index.ts

@@ -9,8 +9,8 @@ export const BUILTIN_MASK_ID = 100000;
 
 export const BUILTIN_MASK_STORE = {
   buildinId: BUILTIN_MASK_ID,
-  masks: {} as Record<number, BuiltinMask>,
-  get(id?: number) {
+  masks: {} as Record<string, BuiltinMask>,
+  get(id?: string) {
     if (!id) return undefined;
     return this.masks[id] as Mask | undefined;
   },

+ 24 - 23
app/store/chat.ts

@@ -16,18 +16,19 @@ import { api, RequestMessage } from "../client/api";
 import { ChatControllerPool } from "../client/controller";
 import { prettyObject } from "../utils/format";
 import { estimateTokenLength } from "../utils/token";
+import { nanoid } from "nanoid";
 
 export type ChatMessage = RequestMessage & {
   date: string;
   streaming?: boolean;
   isError?: boolean;
-  id?: number;
+  id: string;
   model?: ModelType;
 };
 
 export function createMessage(override: Partial<ChatMessage>): ChatMessage {
   return {
-    id: Date.now(),
+    id: nanoid(),
     date: new Date().toLocaleString(),
     role: "user",
     content: "",
@@ -42,7 +43,7 @@ export interface ChatStat {
 }
 
 export interface ChatSession {
-  id: number;
+  id: string;
   topic: string;
 
   memoryPrompt: string;
@@ -63,7 +64,7 @@ export const BOT_HELLO: ChatMessage = createMessage({
 
 function createEmptySession(): ChatSession {
   return {
-    id: Date.now() + Math.random(),
+    id: nanoid(),
     topic: DEFAULT_TOPIC,
     memoryPrompt: "",
     messages: [],
@@ -82,7 +83,6 @@ function createEmptySession(): ChatSession {
 interface ChatStore {
   sessions: ChatSession[];
   currentSessionIndex: number;
-  globalId: number;
   clearSessions: () => void;
   moveSession: (from: number, to: number) => void;
   selectSession: (index: number) => void;
@@ -139,7 +139,6 @@ export const useChatStore = create<ChatStore>()(
     (set, get) => ({
       sessions: [createEmptySession()],
       currentSessionIndex: 0,
-      globalId: 0,
 
       clearSessions() {
         set(() => ({
@@ -182,9 +181,6 @@ export const useChatStore = create<ChatStore>()(
       newSession(mask) {
         const session = createEmptySession();
 
-        set(() => ({ globalId: get().globalId + 1 }));
-        session.id = get().globalId;
-
         if (mask) {
           const config = useAppConfig.getState();
           const globalModelConfig = config.modelConfig;
@@ -300,7 +296,6 @@ export const useChatStore = create<ChatStore>()(
         // get recent messages
         const recentMessages = get().getMessagesWithMemory();
         const sendMessages = recentMessages.concat(userMessage);
-        const sessionIndex = get().currentSessionIndex;
         const messageIndex = get().currentSession().messages.length + 1;
 
         // save user's and bot's message
@@ -334,10 +329,7 @@ export const useChatStore = create<ChatStore>()(
               botMessage.content = message;
               get().onNewMessage(botMessage);
             }
-            ChatControllerPool.remove(
-              sessionIndex,
-              botMessage.id ?? messageIndex,
-            );
+            ChatControllerPool.remove(session.id, botMessage.id);
           },
           onError(error) {
             const isAborted = error.message.includes("aborted");
@@ -354,7 +346,7 @@ export const useChatStore = create<ChatStore>()(
               session.messages = session.messages.concat();
             });
             ChatControllerPool.remove(
-              sessionIndex,
+              session.id,
               botMessage.id ?? messageIndex,
             );
 
@@ -363,7 +355,7 @@ export const useChatStore = create<ChatStore>()(
           onController(controller) {
             // collect controller for stop/retry
             ChatControllerPool.addController(
-              sessionIndex,
+              session.id,
               botMessage.id ?? messageIndex,
               controller,
             );
@@ -556,11 +548,13 @@ export const useChatStore = create<ChatStore>()(
           modelConfig.sendMemory
         ) {
           api.llm.chat({
-            messages: toBeSummarizedMsgs.concat({
-              role: "system",
-              content: Locale.Store.Prompt.Summarize,
-              date: "",
-            }),
+            messages: toBeSummarizedMsgs.concat(
+              createMessage({
+                role: "system",
+                content: Locale.Store.Prompt.Summarize,
+                date: "",
+              }),
+            ),
             config: { ...modelConfig, stream: true },
             onUpdate(message) {
               session.memoryPrompt = message;
@@ -597,13 +591,12 @@ export const useChatStore = create<ChatStore>()(
     }),
     {
       name: StoreKey.Chat,
-      version: 2,
+      version: 3,
       migrate(persistedState, version) {
         const state = persistedState as any;
         const newState = JSON.parse(JSON.stringify(state)) as ChatStore;
 
         if (version < 2) {
-          newState.globalId = 0;
           newState.sessions = [];
 
           const oldSessions = state.sessions;
@@ -618,6 +611,14 @@ export const useChatStore = create<ChatStore>()(
           }
         }
 
+        if (version < 3) {
+          // migrate id to nanoid
+          newState.sessions.forEach((s) => {
+            s.id = nanoid();
+            s.messages.forEach((m) => (m.id = nanoid()));
+          });
+        }
+
         return newState;
       },
     },

+ 23 - 12
app/store/mask.ts

@@ -5,9 +5,11 @@ import { getLang, Lang } from "../locales";
 import { DEFAULT_TOPIC, ChatMessage } from "./chat";
 import { ModelConfig, useAppConfig } from "./config";
 import { StoreKey } from "../constant";
+import { nanoid } from "nanoid";
 
 export type Mask = {
-  id: number;
+  id: string;
+  createdAt: number;
   avatar: string;
   name: string;
   hideContext?: boolean;
@@ -19,25 +21,23 @@ export type Mask = {
 };
 
 export const DEFAULT_MASK_STATE = {
-  masks: {} as Record<number, Mask>,
-  globalMaskId: 0,
+  masks: {} as Record<string, Mask>,
 };
 
 export type MaskState = typeof DEFAULT_MASK_STATE;
 type MaskStore = MaskState & {
   create: (mask?: Partial<Mask>) => Mask;
-  update: (id: number, updater: (mask: Mask) => void) => void;
-  delete: (id: number) => void;
+  update: (id: string, updater: (mask: Mask) => void) => void;
+  delete: (id: string) => void;
   search: (text: string) => Mask[];
-  get: (id?: number) => Mask | null;
+  get: (id?: string) => Mask | null;
   getAll: () => Mask[];
 };
 
-export const DEFAULT_MASK_ID = 1145141919810;
 export const DEFAULT_MASK_AVATAR = "gpt-bot";
 export const createEmptyMask = () =>
   ({
-    id: DEFAULT_MASK_ID,
+    id: nanoid(),
     avatar: DEFAULT_MASK_AVATAR,
     name: DEFAULT_TOPIC,
     context: [],
@@ -45,6 +45,7 @@ export const createEmptyMask = () =>
     modelConfig: { ...useAppConfig.getState().modelConfig },
     lang: getLang(),
     builtin: false,
+    createdAt: Date.now(),
   } as Mask);
 
 export const useMaskStore = create<MaskStore>()(
@@ -53,9 +54,8 @@ export const useMaskStore = create<MaskStore>()(
       ...DEFAULT_MASK_STATE,
 
       create(mask) {
-        set(() => ({ globalMaskId: get().globalMaskId + 1 }));
-        const id = get().globalMaskId;
         const masks = get().masks;
+        const id = nanoid();
         masks[id] = {
           ...createEmptyMask(),
           ...mask,
@@ -87,7 +87,7 @@ export const useMaskStore = create<MaskStore>()(
       },
       getAll() {
         const userMasks = Object.values(get().masks).sort(
-          (a, b) => b.id - a.id,
+          (a, b) => b.createdAt - a.createdAt,
         );
         const config = useAppConfig.getState();
         if (config.hideBuiltinMasks) return userMasks;
@@ -109,7 +109,18 @@ export const useMaskStore = create<MaskStore>()(
     }),
     {
       name: StoreKey.Mask,
-      version: 2,
+      version: 3,
+
+      migrate(state, version) {
+        const newState = JSON.parse(JSON.stringify(state)) as MaskState;
+
+        // migrate mask id to nanoid
+        if (version < 3) {
+          Object.values(newState.masks).forEach((m) => (m.id = nanoid()));
+        }
+
+        return newState as any;
+      },
     },
   ),
 );

+ 29 - 13
app/store/prompt.ts

@@ -3,24 +3,25 @@ import { persist } from "zustand/middleware";
 import Fuse from "fuse.js";
 import { getLang } from "../locales";
 import { StoreKey } from "../constant";
+import { nanoid } from "nanoid";
 
 export interface Prompt {
-  id?: number;
+  id: string;
   isUser?: boolean;
   title: string;
   content: string;
+  createdAt: number;
 }
 
 export interface PromptStore {
   counter: number;
-  latestId: number;
-  prompts: Record<number, Prompt>;
+  prompts: Record<string, Prompt>;
 
-  add: (prompt: Prompt) => number;
-  get: (id: number) => Prompt | undefined;
-  remove: (id: number) => void;
+  add: (prompt: Prompt) => string;
+  get: (id: string) => Prompt | undefined;
+  remove: (id: string) => void;
   search: (text: string) => Prompt[];
-  update: (id: number, updater: (prompt: Prompt) => void) => void;
+  update: (id: string, updater: (prompt: Prompt) => void) => void;
 
   getUserPrompts: () => Prompt[];
 }
@@ -46,7 +47,7 @@ export const SearchService = {
     this.ready = true;
   },
 
-  remove(id: number) {
+  remove(id: string) {
     this.userEngine.remove((doc) => doc.id === id);
   },
 
@@ -70,8 +71,9 @@ export const usePromptStore = create<PromptStore>()(
 
       add(prompt) {
         const prompts = get().prompts;
-        prompt.id = get().latestId + 1;
+        prompt.id = nanoid();
         prompt.isUser = true;
+        prompt.createdAt = Date.now();
         prompts[prompt.id] = prompt;
 
         set(() => ({
@@ -105,11 +107,13 @@ export const usePromptStore = create<PromptStore>()(
 
       getUserPrompts() {
         const userPrompts = Object.values(get().prompts ?? {});
-        userPrompts.sort((a, b) => (b.id && a.id ? b.id - a.id : 0));
+        userPrompts.sort((a, b) =>
+          b.id && a.id ? b.createdAt - a.createdAt : 0,
+        );
         return userPrompts;
       },
 
-      update(id: number, updater) {
+      update(id, updater) {
         const prompt = get().prompts[id] ?? {
           title: "",
           content: "",
@@ -134,7 +138,18 @@ export const usePromptStore = create<PromptStore>()(
     }),
     {
       name: StoreKey.Prompt,
-      version: 1,
+      version: 3,
+
+      migrate(state, version) {
+        const newState = JSON.parse(JSON.stringify(state)) as PromptStore;
+
+        if (version < 3) {
+          Object.values(newState.prompts).forEach((p) => (p.id = nanoid()));
+        }
+
+        return newState;
+      },
+
       onRehydrateStorage(state) {
         const PROMPT_URL = "./prompts.json";
 
@@ -152,9 +167,10 @@ export const usePromptStore = create<PromptStore>()(
                 return promptList.map(
                   ([title, content]) =>
                     ({
-                      id: Math.random(),
+                      id: nanoid(),
                       title,
                       content,
+                      createdAt: Date.now(),
                     } as Prompt),
                 );
               },

Some files were not shown because too many files changed in this diff