chore: remove unused ComponentExample and associated imports and auto title generate
This commit is contained in:
parent
b83eeab603
commit
9a4f71c939
8 changed files with 226 additions and 1352 deletions
|
|
@ -2,7 +2,6 @@ import { createRouter } from "@tanstack/react-router";
|
|||
import { Route as rootRoute } from "./routes/__root";
|
||||
import { Route as chatRoute } from "./routes/chat";
|
||||
import { Route as gridTestRoute } from "./routes/grid-test";
|
||||
import { Route as homeRoute } from "./routes/home";
|
||||
import { Route as loginRoute } from "./routes/login";
|
||||
import { Route as onboardingRoute } from "./routes/onboarding";
|
||||
import { Route as exportRoute } from "./routes/export";
|
||||
|
|
@ -10,7 +9,6 @@ import { Route as signupRoute } from "./routes/signup";
|
|||
import { Route as studioRoute } from "./routes/studio";
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
homeRoute,
|
||||
onboardingRoute,
|
||||
loginRoute,
|
||||
signupRoute,
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import { ComponentExample } from "@/components/component-example";
|
||||
import { createRoute } from "@tanstack/react-router";
|
||||
import { requireAuth } from "../auth-guards";
|
||||
import { Route as rootRoute } from "./__root";
|
||||
|
||||
export const Route = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/",
|
||||
beforeLoad: () => requireAuth(),
|
||||
component: HomePage,
|
||||
});
|
||||
|
||||
function HomePage() {
|
||||
return <ComponentExample />;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -127,6 +127,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
const threadKey = unstable_threadId || "__default";
|
||||
let waitingFirstChunk = true;
|
||||
useChatRuntimeStore.getState().setThreadWarming(threadKey, true);
|
||||
useChatRuntimeStore.getState().setThreadRunning(threadKey, true);
|
||||
let cumulativeText = "";
|
||||
let reasoningStartAt: number | null = null;
|
||||
let reasoningDuration = 0;
|
||||
|
|
@ -179,6 +180,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
if (waitingFirstChunk) {
|
||||
useChatRuntimeStore.getState().setThreadWarming(threadKey, false);
|
||||
}
|
||||
useChatRuntimeStore.getState().setThreadRunning(threadKey, false);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -209,6 +209,8 @@ export function ChatPage(): ReactElement {
|
|||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const inferenceParams = useChatRuntimeStore((state) => state.params);
|
||||
const setInferenceParams = useChatRuntimeStore((state) => state.setParams);
|
||||
const autoTitle = useChatRuntimeStore((state) => state.autoTitle);
|
||||
const setAutoTitle = useChatRuntimeStore((state) => state.setAutoTitle);
|
||||
const modelsFromStore = useChatRuntimeStore((state) => state.models);
|
||||
const lorasFromStore = useChatRuntimeStore((state) => state.loras);
|
||||
const modelsError = useChatRuntimeStore((state) => state.modelsError);
|
||||
|
|
@ -326,6 +328,8 @@ export function ChatPage(): ReactElement {
|
|||
open={settingsOpen}
|
||||
params={inferenceParams}
|
||||
onParamsChange={setInferenceParams}
|
||||
autoTitle={autoTitle}
|
||||
onAutoTitleChange={setAutoTitle}
|
||||
/>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
DEFAULT_INFERENCE_PARAMS,
|
||||
type InferenceParams,
|
||||
} from "./types/runtime";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export const defaultInferenceParams = DEFAULT_INFERENCE_PARAMS;
|
||||
export type { InferenceParams } from "./types/runtime";
|
||||
|
|
@ -143,12 +144,16 @@ interface ChatSettingsPanelProps {
|
|||
open: boolean;
|
||||
params: InferenceParams;
|
||||
onParamsChange: (params: InferenceParams) => void;
|
||||
autoTitle: boolean;
|
||||
onAutoTitleChange: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export function ChatSettingsPanel({
|
||||
open,
|
||||
params,
|
||||
onParamsChange,
|
||||
autoTitle,
|
||||
onAutoTitleChange,
|
||||
}: ChatSettingsPanelProps) {
|
||||
const [presets, setPresets] = useState<Preset[]>(BUILTIN_PRESETS);
|
||||
const [activePreset, setActivePreset] = useState("Default");
|
||||
|
|
@ -315,9 +320,18 @@ export function ChatSettingsPanel({
|
|||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection icon={Settings02Icon} label="Settings">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No additional settings yet.
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-3 py-1">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium">Auto title</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
Generate short title after reply.
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoTitle}
|
||||
onCheckedChange={onAutoTitleChange}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
SimpleTextAttachmentAdapter,
|
||||
type ThreadHistoryAdapter,
|
||||
type ThreadMessage,
|
||||
type ThreadUserMessagePart,
|
||||
WebSpeechDictationAdapter,
|
||||
type unstable_RemoteThreadListAdapter,
|
||||
useAui,
|
||||
|
|
@ -23,8 +22,10 @@ import { createAssistantStream } from "assistant-stream";
|
|||
import mammoth from "mammoth";
|
||||
import { type ReactElement, type ReactNode, useEffect, useMemo } from "react";
|
||||
import { extractText, getDocumentProxy } from "unpdf";
|
||||
import { authFetch } from "@/features/auth";
|
||||
import { createOpenAIStreamAdapter } from "./api/chat-adapter";
|
||||
import { db } from "./db";
|
||||
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
|
||||
import type { MessageRecord, ModelType } from "./types";
|
||||
|
||||
const DEFAULT_SUGGESTIONS = [
|
||||
|
|
@ -149,6 +150,81 @@ class DocxAttachmentAdapter implements AttachmentAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
function clip(input: string, maxLen: number): string {
|
||||
const text = input.replace(/\s+/g, " ").trim();
|
||||
if (text.length <= maxLen) return text;
|
||||
return text.slice(0, maxLen).trimEnd();
|
||||
}
|
||||
|
||||
function extractTextParts(m: ThreadMessage | undefined): string {
|
||||
if (!m) return "";
|
||||
const content = Array.isArray(m.content) ? m.content : [];
|
||||
return content
|
||||
.filter((p): p is Extract<typeof p, { type: "text" }> => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function generateTitleWithModel(payload: {
|
||||
userText: string;
|
||||
}): Promise<string | null> {
|
||||
const params = useChatRuntimeStore.getState().params;
|
||||
if (!params.checkpoint) return null;
|
||||
|
||||
const user = clip(payload.userText, 256);
|
||||
const parts: string[] = [user];
|
||||
|
||||
function normalizeTitle(raw: string): string | null {
|
||||
let title = raw.split(/\r?\n/, 1)[0] ?? "";
|
||||
title = title.replace(/^\s*title\s*:\s*/i, "");
|
||||
title = title.replace(/[^\x20-\x7E]+/g, " ");
|
||||
title = title.replace(/["'`]+/g, "");
|
||||
title = title.replace(/[.!?:;,]+/g, " ");
|
||||
title = title.replace(/\s+/g, " ").trim();
|
||||
|
||||
// Model echo fail-safe.
|
||||
if (/\b(user|base|lora|assistant)\s*:/i.test(title)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const words = title.split(" ").filter(Boolean).slice(0, 6);
|
||||
const joined = words.join(" ").trim();
|
||||
if (!joined) return null;
|
||||
return joined.length > 60 ? joined.slice(0, 60).trimEnd() : joined;
|
||||
}
|
||||
|
||||
const response = await authFetch("/api/inference/chat/completions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: params.checkpoint,
|
||||
stream: false,
|
||||
temperature: 0.2,
|
||||
top_p: 0.9,
|
||||
max_tokens: 24,
|
||||
top_k: 40,
|
||||
repetition_penalty: 1.05,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"Write 1 concise chat title for the user's message. Rules: 2-6 words, no quotes, no punctuation, ASCII only, do not echo input. Output title only.",
|
||||
},
|
||||
{ role: "user", content: parts.join("\n") },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const body = (await response.json().catch(() => null)) as any;
|
||||
if (!response.ok) return null;
|
||||
const raw: string | undefined = body?.choices?.[0]?.message?.content;
|
||||
if (!raw) return null;
|
||||
return normalizeTitle(raw);
|
||||
}
|
||||
|
||||
const inflightTitleByKey = new Set<string>();
|
||||
|
||||
function toThreadMessage(m: MessageRecord): ThreadMessage {
|
||||
const base = {
|
||||
id: m.id,
|
||||
|
|
@ -245,32 +321,98 @@ function createDexieAdapter(
|
|||
},
|
||||
|
||||
async generateTitle(remoteId: string, messages: readonly ThreadMessage[]) {
|
||||
const autoTitle = useChatRuntimeStore.getState().autoTitle;
|
||||
const thread = await db.threads.get(remoteId);
|
||||
if (!thread) {
|
||||
return createAssistantStream((c) => {
|
||||
c.appendText("New Chat");
|
||||
c.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Only generate once per thread/pair.
|
||||
if (thread.title && thread.title !== "New Chat") {
|
||||
return createAssistantStream((c) => {
|
||||
c.appendText(thread.title);
|
||||
c.close();
|
||||
});
|
||||
}
|
||||
|
||||
const firstUser = messages.find((m) => m.role === "user");
|
||||
const textParts =
|
||||
firstUser?.content.filter(
|
||||
(part): part is Extract<ThreadUserMessagePart, { type: "text" }> =>
|
||||
part.type === "text",
|
||||
) ?? [];
|
||||
const text = textParts.map((part) => part.text).join("") || "New Chat";
|
||||
const title = text.slice(0, 60) + (text.length > 60 ? "..." : "");
|
||||
const userText = extractTextParts(firstUser) || "New Chat";
|
||||
|
||||
await db.threads.update(remoteId, { title });
|
||||
if (!autoTitle) {
|
||||
const title = userText.slice(0, 60) + (userText.length > 60 ? "..." : "");
|
||||
await db.threads.update(remoteId, { title });
|
||||
if (pairId) {
|
||||
const paired = await db.threads
|
||||
.where("pairId")
|
||||
.equals(pairId)
|
||||
.filter((t) => t.id !== remoteId)
|
||||
.first();
|
||||
if (paired) await db.threads.update(paired.id, { title });
|
||||
}
|
||||
return createAssistantStream((c) => {
|
||||
c.appendText(title);
|
||||
c.close();
|
||||
});
|
||||
}
|
||||
|
||||
const key = pairId ? `pair:${pairId}` : `thread:${remoteId}`;
|
||||
if (inflightTitleByKey.has(key)) {
|
||||
return createAssistantStream((c) => {
|
||||
c.appendText(thread.title || "New Chat");
|
||||
c.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Compare: wait until both threads done.
|
||||
if (pairId) {
|
||||
const paired = await db.threads
|
||||
.where("pairId")
|
||||
.equals(pairId)
|
||||
.filter((t) => t.id !== remoteId)
|
||||
.first();
|
||||
|
||||
if (paired) {
|
||||
await db.threads.update(paired.id, { title });
|
||||
const running = useChatRuntimeStore.getState().runningByThreadId;
|
||||
if (running[paired.id]) {
|
||||
setTimeout(() => {
|
||||
void createDexieAdapter(modelType, pairId).generateTitle(remoteId, messages);
|
||||
}, 600);
|
||||
return createAssistantStream((c) => {
|
||||
c.appendText(thread.title || "New Chat");
|
||||
c.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createAssistantStream((controller) => {
|
||||
controller.appendText(title);
|
||||
controller.close();
|
||||
});
|
||||
inflightTitleByKey.add(key);
|
||||
try {
|
||||
const title =
|
||||
(await generateTitleWithModel({
|
||||
userText,
|
||||
})) ||
|
||||
(userText.slice(0, 60) + (userText.length > 60 ? "..." : ""));
|
||||
|
||||
await db.threads.update(remoteId, { title });
|
||||
if (pairId) {
|
||||
const paired = await db.threads
|
||||
.where("pairId")
|
||||
.equals(pairId)
|
||||
.filter((t) => t.id !== remoteId)
|
||||
.first();
|
||||
if (paired) await db.threads.update(paired.id, { title });
|
||||
}
|
||||
|
||||
return createAssistantStream((c) => {
|
||||
c.appendText(title);
|
||||
c.close();
|
||||
});
|
||||
} finally {
|
||||
inflightTitleByKey.delete(key);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,46 @@ import {
|
|||
type InferenceParams,
|
||||
} from "../types/runtime";
|
||||
|
||||
const AUTO_TITLE_KEY = "unsloth_chat_auto_title";
|
||||
|
||||
function canUseStorage(): boolean {
|
||||
return typeof window !== "undefined";
|
||||
}
|
||||
|
||||
function loadBool(key: string, fallback: boolean): boolean {
|
||||
if (!canUseStorage()) return fallback;
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw === null) return fallback;
|
||||
return raw === "true";
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function saveBool(key: string, value: boolean): void {
|
||||
if (!canUseStorage()) return;
|
||||
try {
|
||||
localStorage.setItem(key, value ? "true" : "false");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
type ChatRuntimeStore = {
|
||||
params: InferenceParams;
|
||||
models: ChatModelSummary[];
|
||||
loras: ChatLoraSummary[];
|
||||
warmingByThreadId: Record<string, boolean>;
|
||||
runningByThreadId: Record<string, boolean>;
|
||||
autoTitle: boolean;
|
||||
modelsError: string | null;
|
||||
setParams: (params: InferenceParams) => void;
|
||||
setModels: (models: ChatModelSummary[]) => void;
|
||||
setLoras: (loras: ChatLoraSummary[]) => void;
|
||||
setThreadWarming: (threadId: string, warming: boolean) => void;
|
||||
setThreadRunning: (threadId: string, running: boolean) => void;
|
||||
setAutoTitle: (enabled: boolean) => void;
|
||||
setModelsError: (error: string | null) => void;
|
||||
setCheckpoint: (modelId: string) => void;
|
||||
clearCheckpoint: () => void;
|
||||
|
|
@ -26,6 +56,8 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set) => ({
|
|||
models: [],
|
||||
loras: [],
|
||||
warmingByThreadId: {},
|
||||
runningByThreadId: {},
|
||||
autoTitle: loadBool(AUTO_TITLE_KEY, true),
|
||||
modelsError: null,
|
||||
setParams: (params) => set({ params }),
|
||||
setModels: (models) => set({ models }),
|
||||
|
|
@ -40,6 +72,21 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set) => ({
|
|||
}
|
||||
return { warmingByThreadId: next };
|
||||
}),
|
||||
setThreadRunning: (threadId, running) =>
|
||||
set((state) => {
|
||||
const next = { ...state.runningByThreadId };
|
||||
if (running) {
|
||||
next[threadId] = true;
|
||||
} else {
|
||||
delete next[threadId];
|
||||
}
|
||||
return { runningByThreadId: next };
|
||||
}),
|
||||
setAutoTitle: (autoTitle) =>
|
||||
set(() => {
|
||||
saveBool(AUTO_TITLE_KEY, autoTitle);
|
||||
return { autoTitle };
|
||||
}),
|
||||
setModelsError: (modelsError) => set({ modelsError }),
|
||||
setCheckpoint: (modelId) =>
|
||||
set((state) => ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue