a98096a042
- Created .gitignore to exclude build artifacts and dependencies. - Added index.html as the main entry point for the application. - Included LICENSE file with Apache 2.0 terms. - Initialized package.json and package-lock.json for project dependencies. - Added pnpm-lock.yaml for package management. - Created QUICKSTART.md for setup instructions. - Added README.md and README.zh-CN.md for project documentation in English and Chinese.
283 lines
7.2 KiB
TypeScript
283 lines
7.2 KiB
TypeScript
// Project / conversation / message / tab persistence — backed by the
|
|
// daemon's SQLite store. All writes round-trip through HTTP so projects
|
|
// stay coherent across multiple browser tabs and across restarts.
|
|
//
|
|
// These helpers fail soft (returning null / [] on transport errors) so
|
|
// the UI can stay rendered when the daemon is briefly unreachable.
|
|
|
|
import type {
|
|
ChatMessage,
|
|
Conversation,
|
|
OpenTabsState,
|
|
Project,
|
|
ProjectMetadata,
|
|
ProjectTemplate,
|
|
} from '../types';
|
|
|
|
export async function listProjects(): Promise<Project[]> {
|
|
try {
|
|
const resp = await fetch('/api/projects');
|
|
if (!resp.ok) return [];
|
|
const json = (await resp.json()) as { projects: Project[] };
|
|
return json.projects ?? [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getProject(id: string): Promise<Project | null> {
|
|
try {
|
|
const resp = await fetch(`/api/projects/${encodeURIComponent(id)}`);
|
|
if (!resp.ok) return null;
|
|
const json = (await resp.json()) as { project: Project };
|
|
return json.project;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function createProject(input: {
|
|
name: string;
|
|
skillId: string | null;
|
|
designSystemId: string | null;
|
|
pendingPrompt?: string;
|
|
metadata?: ProjectMetadata;
|
|
}): Promise<{ project: Project; conversationId: string } | null> {
|
|
try {
|
|
const id = crypto.randomUUID();
|
|
const resp = await fetch('/api/projects', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, ...input }),
|
|
});
|
|
if (!resp.ok) return null;
|
|
return (await resp.json()) as { project: Project; conversationId: string };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ---------- templates ----------
|
|
|
|
export async function listTemplates(): Promise<ProjectTemplate[]> {
|
|
try {
|
|
const resp = await fetch('/api/templates');
|
|
if (!resp.ok) return [];
|
|
const json = (await resp.json()) as { templates: ProjectTemplate[] };
|
|
return json.templates ?? [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getTemplate(id: string): Promise<ProjectTemplate | null> {
|
|
try {
|
|
const resp = await fetch(`/api/templates/${encodeURIComponent(id)}`);
|
|
if (!resp.ok) return null;
|
|
const json = (await resp.json()) as { template: ProjectTemplate };
|
|
return json.template;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function saveTemplate(input: {
|
|
name: string;
|
|
description?: string;
|
|
sourceProjectId: string;
|
|
}): Promise<ProjectTemplate | null> {
|
|
try {
|
|
const resp = await fetch('/api/templates', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
if (!resp.ok) return null;
|
|
const json = (await resp.json()) as { template: ProjectTemplate };
|
|
return json.template;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteTemplate(id: string): Promise<boolean> {
|
|
try {
|
|
const resp = await fetch(`/api/templates/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
return resp.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function patchProject(
|
|
id: string,
|
|
patch: Partial<Project>,
|
|
): Promise<Project | null> {
|
|
try {
|
|
const resp = await fetch(`/api/projects/${encodeURIComponent(id)}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch),
|
|
});
|
|
if (!resp.ok) return null;
|
|
const json = (await resp.json()) as { project: Project };
|
|
return json.project;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteProject(id: string): Promise<boolean> {
|
|
try {
|
|
const resp = await fetch(`/api/projects/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
return resp.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---------- conversations ----------
|
|
|
|
export async function listConversations(
|
|
projectId: string,
|
|
): Promise<Conversation[]> {
|
|
try {
|
|
const resp = await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/conversations`,
|
|
);
|
|
if (!resp.ok) return [];
|
|
const json = (await resp.json()) as { conversations: Conversation[] };
|
|
return json.conversations ?? [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function createConversation(
|
|
projectId: string,
|
|
title?: string,
|
|
): Promise<Conversation | null> {
|
|
try {
|
|
const resp = await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/conversations`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title }),
|
|
},
|
|
);
|
|
if (!resp.ok) return null;
|
|
const json = (await resp.json()) as { conversation: Conversation };
|
|
return json.conversation;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function patchConversation(
|
|
projectId: string,
|
|
conversationId: string,
|
|
patch: Partial<Conversation>,
|
|
): Promise<Conversation | null> {
|
|
try {
|
|
const resp = await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(conversationId)}`,
|
|
{
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch),
|
|
},
|
|
);
|
|
if (!resp.ok) return null;
|
|
const json = (await resp.json()) as { conversation: Conversation };
|
|
return json.conversation;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteConversation(
|
|
projectId: string,
|
|
conversationId: string,
|
|
): Promise<boolean> {
|
|
try {
|
|
const resp = await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(conversationId)}`,
|
|
{ method: 'DELETE' },
|
|
);
|
|
return resp.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---------- messages ----------
|
|
|
|
export async function listMessages(
|
|
projectId: string,
|
|
conversationId: string,
|
|
): Promise<ChatMessage[]> {
|
|
try {
|
|
const resp = await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
);
|
|
if (!resp.ok) return [];
|
|
const json = (await resp.json()) as { messages: ChatMessage[] };
|
|
return json.messages ?? [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function saveMessage(
|
|
projectId: string,
|
|
conversationId: string,
|
|
message: ChatMessage,
|
|
): Promise<void> {
|
|
try {
|
|
await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(message.id)}`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(message),
|
|
},
|
|
);
|
|
} catch {
|
|
// best-effort persistence — UI keeps the message in-memory either way
|
|
}
|
|
}
|
|
|
|
// ---------- tabs ----------
|
|
|
|
export async function loadTabs(projectId: string): Promise<OpenTabsState> {
|
|
try {
|
|
const resp = await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/tabs`,
|
|
);
|
|
if (!resp.ok) return { tabs: [], active: null };
|
|
return (await resp.json()) as OpenTabsState;
|
|
} catch {
|
|
return { tabs: [], active: null };
|
|
}
|
|
}
|
|
|
|
export async function saveTabs(
|
|
projectId: string,
|
|
state: OpenTabsState,
|
|
): Promise<void> {
|
|
try {
|
|
await fetch(`/api/projects/${encodeURIComponent(projectId)}/tabs`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(state),
|
|
});
|
|
} catch {
|
|
// best-effort
|
|
}
|
|
}
|