chore: remove unused ComponentExample and associated imports and auto title generate

This commit is contained in:
Shine1i 2026-02-15 18:08:46 +01:00
commit 9a4f71c939
8 changed files with 226 additions and 1352 deletions

View file

@ -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,

View file

@ -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

View file

@ -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);
}
},
};

View file

@ -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>

View file

@ -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>

View file

@ -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);
}
},
};
}

View file

@ -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) => ({