All checks were successful
CI / Format (push) Successful in 46s
CI / CUDA type-check (push) Successful in 1m40s
CI / Clippy (push) Successful in 3m20s
CI / Test (push) Successful in 8m3s
CI / Build cortex SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
The beta centerpiece: a chat workspace at `/` with zero server-side history. Everything personal lives in the browser (Dexie/IndexedDB); inference streams from the mesh router. - data/db.ts: Dexie schema (projects, conversations[owner:'anon'|accountId], messages, meta) — `owner` namespaces anonymous vs account data; claimAnonymousData() (repositories) re-owns anon data on login (F4), still client-side. - data/repositories.ts: typed CRUD + ordered queries (projects, conversations, messages) used reactively via useLiveQuery. - lib/fingerprint.ts: FingerprintJS OSS, cached in meta (best-effort, never auth) — namespaces anon data + a soft throttle id. - lib/chatClient.ts: streamChatCompletion → POST /v1/chat/completions stream:true; parses the SSE byte stream incrementally (data:/[DONE]/ partial frames), surfaces the OpenAI envelope error.code, AbortController for Stop. - lib/useChat.ts: persists the user turn, opens a streaming assistant message, appends deltas to Dexie live, titles the conversation, finalizes on done/error. - pages/Chat.tsx: sidebar (new chat / new project, conversations grouped by project + Unsorted), live-updating thread with streaming + error rendering, composer with send/stop. Anonymous mode: no bearer + VITE_ANON_MODEL + a client message cap with a sign-up nudge. Routed at `/`. - chat i18n namespace extended (newChat/newProject/unsorted/emptyState/ anonBanner/signUp/stop) across all 33 languages (parity holds). Validated: npm run lint, typecheck, build all green; i18n:check consistent. (Full browser flow exercised in F6 verify.) Explicit-path commit; no node_modules/dist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F6o3ddqmYNh9kzdwq6eowh
155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
// Typed CRUD + queries over the Dexie store. UI components use the
|
|
// `useLiveQuery` hook (dexie-react-hooks) with the list helpers here so the
|
|
// sidebar/thread react to writes automatically.
|
|
|
|
import Dexie from "dexie";
|
|
import {
|
|
db,
|
|
type Conversation,
|
|
type Message,
|
|
type MessageRole,
|
|
type Project,
|
|
} from "./db";
|
|
|
|
function uuid(): string {
|
|
return crypto.randomUUID();
|
|
}
|
|
function now(): number {
|
|
return Date.now();
|
|
}
|
|
|
|
// ── projects ────────────────────────────────────────────────────────
|
|
|
|
export async function listProjects(owner: string): Promise<Project[]> {
|
|
const rows = await db.projects.where({ owner }).toArray();
|
|
return rows
|
|
.filter((p) => !p.archived)
|
|
.sort((a, b) => a.sortOrder - b.sortOrder || a.createdAt - b.createdAt);
|
|
}
|
|
|
|
export async function createProject(owner: string, name: string): Promise<string> {
|
|
const id = uuid();
|
|
const ts = now();
|
|
await db.projects.add({
|
|
id,
|
|
owner,
|
|
name,
|
|
createdAt: ts,
|
|
updatedAt: ts,
|
|
archived: false,
|
|
sortOrder: ts,
|
|
});
|
|
return id;
|
|
}
|
|
|
|
export async function renameProject(id: string, name: string): Promise<void> {
|
|
await db.projects.update(id, { name, updatedAt: now() });
|
|
}
|
|
|
|
export async function archiveProject(id: string): Promise<void> {
|
|
// Detach its conversations to "Unsorted" so nothing is orphaned.
|
|
await db.transaction("rw", db.projects, db.conversations, async () => {
|
|
await db.projects.update(id, { archived: true, updatedAt: now() });
|
|
const convs = await db.conversations.where({ projectId: id }).toArray();
|
|
await Promise.all(
|
|
convs.map((c) => db.conversations.update(c.id, { projectId: null })),
|
|
);
|
|
});
|
|
}
|
|
|
|
// ── conversations ───────────────────────────────────────────────────
|
|
|
|
export async function listConversations(owner: string): Promise<Conversation[]> {
|
|
const rows = await db.conversations.where({ owner }).toArray();
|
|
return rows.sort(
|
|
(a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt,
|
|
);
|
|
}
|
|
|
|
export async function createConversation(
|
|
owner: string,
|
|
model: string,
|
|
projectId: string | null = null,
|
|
title = "New chat",
|
|
): Promise<string> {
|
|
const id = uuid();
|
|
const ts = now();
|
|
await db.conversations.add({
|
|
id,
|
|
owner,
|
|
projectId,
|
|
title,
|
|
model,
|
|
createdAt: ts,
|
|
updatedAt: ts,
|
|
pinned: false,
|
|
});
|
|
return id;
|
|
}
|
|
|
|
export async function renameConversation(id: string, title: string): Promise<void> {
|
|
await db.conversations.update(id, { title, updatedAt: now() });
|
|
}
|
|
|
|
export async function moveConversation(
|
|
id: string,
|
|
projectId: string | null,
|
|
): Promise<void> {
|
|
await db.conversations.update(id, { projectId, updatedAt: now() });
|
|
}
|
|
|
|
export async function deleteConversation(id: string): Promise<void> {
|
|
await db.transaction("rw", db.conversations, db.messages, async () => {
|
|
await db.messages.where({ conversationId: id }).delete();
|
|
await db.conversations.delete(id);
|
|
});
|
|
}
|
|
|
|
// ── messages ────────────────────────────────────────────────────────
|
|
|
|
export async function listMessages(conversationId: string): Promise<Message[]> {
|
|
return db.messages
|
|
.where("[conversationId+createdAt]")
|
|
.between([conversationId, Dexie.minKey], [conversationId, Dexie.maxKey])
|
|
.toArray();
|
|
}
|
|
|
|
export async function addMessage(
|
|
conversationId: string,
|
|
role: MessageRole,
|
|
content: string,
|
|
status: Message["status"] = "complete",
|
|
): Promise<string> {
|
|
const id = uuid();
|
|
await db.messages.add({ id, conversationId, role, content, createdAt: now(), status });
|
|
await db.conversations.update(conversationId, { updatedAt: now() });
|
|
return id;
|
|
}
|
|
|
|
export async function appendToMessage(id: string, delta: string): Promise<void> {
|
|
const msg = await db.messages.get(id);
|
|
if (!msg) return;
|
|
await db.messages.update(id, { content: msg.content + delta });
|
|
}
|
|
|
|
export async function finalizeMessage(
|
|
id: string,
|
|
patch: Partial<Pick<Message, "status" | "errorCode" | "promptTokens" | "completionTokens">>,
|
|
): Promise<void> {
|
|
await db.messages.update(id, patch);
|
|
}
|
|
|
|
/** Rewrite all `anon` data to `accountId` on first login (stays local). */
|
|
export async function claimAnonymousData(accountId: string): Promise<void> {
|
|
await db.transaction("rw", db.projects, db.conversations, async () => {
|
|
const projects = await db.projects.where({ owner: "anon" }).toArray();
|
|
await Promise.all(
|
|
projects.map((p) => db.projects.update(p.id, { owner: accountId })),
|
|
);
|
|
const convs = await db.conversations.where({ owner: "anon" }).toArray();
|
|
await Promise.all(
|
|
convs.map((c) => db.conversations.update(c.id, { owner: accountId })),
|
|
);
|
|
});
|
|
}
|