Skip to content

Commit cab2dde

Browse files
committed
Phase 3: 프론트 AI 대화 + 아티팩트 패널 구현
- ai/aiApi.ts: /api/ai/* 전체 API 클라이언트 (프로바이더, 프로필, 대화, OAuth) - ai/aiStore.svelte.ts: AI 상태 관리 (메시지, 프로바이더, 아티팩트, SSE 프로필 구독) - ai/toolRenderer.ts: tool_use 결과를 UI 라벨/아이콘/상태로 변환 - ChatBar.svelte: 하단 접이식 대화 바 (접으면 토글만, 펼치면 전체 대화) - ArtifactPanel.svelte + ArtifactRenderer.svelte: 우측 AI 생성물 패널 - AIChatPanel.svelte: 사이드바 AI 채팅 패널 전면 리라이트 (백엔드 연동) - AppChrome.svelte: 레이아웃에 ChatBar(Footer 위) + ArtifactPanel(우측) 추가 - EditPage.svelte: AI store 초기화/정리 lifecycle 연결
1 parent 8a36201 commit cab2dde

9 files changed

Lines changed: 1751 additions & 65 deletions

File tree

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { apiUrl } from "../basePath";
2+
3+
export interface AiProviderCatalog {
4+
catalog: AiProviderEntry[];
5+
}
6+
7+
export interface AiProviderEntry {
8+
id: string;
9+
label: string;
10+
description: string;
11+
authKind: string;
12+
supportsRoles: string[];
13+
defaultModel: string | null;
14+
}
15+
16+
export interface AiProfilePayload {
17+
version: number;
18+
revision: number;
19+
defaultProvider: string;
20+
providers: Record<string, AiProviderProfilePayload>;
21+
roles: Record<string, AiRoleBinding>;
22+
temperature: number;
23+
maxTokens: number;
24+
ready: boolean;
25+
activeProvider: string | null;
26+
activeModel: string | null;
27+
}
28+
29+
export interface AiProviderProfilePayload {
30+
model: string | null;
31+
baseUrl: string | null;
32+
secretConfigured: boolean;
33+
}
34+
35+
export interface AiRoleBinding {
36+
provider: string | null;
37+
model: string | null;
38+
}
39+
40+
export interface AiConversation {
41+
conversationId: string;
42+
role: string;
43+
}
44+
45+
export interface AiChatResponse {
46+
conversationId: string;
47+
answer: string;
48+
provider: string;
49+
model: string;
50+
usage: Record<string, number> | null;
51+
toolCalls: AiToolCallResult[];
52+
}
53+
54+
export interface AiToolCallResult {
55+
toolCallId: string;
56+
name: string;
57+
result: Record<string, unknown>;
58+
}
59+
60+
export interface OAuthAuthorizeResponse {
61+
authUrl: string;
62+
state: string;
63+
}
64+
65+
export interface OAuthStatusResponse {
66+
done: boolean;
67+
error: string | null;
68+
}
69+
70+
async function parseJson<T>(response: Response, fallback: string): Promise<T> {
71+
if (response.ok) {
72+
return response.json() as Promise<T>;
73+
}
74+
let message = fallback;
75+
try {
76+
const payload = await response.json();
77+
if (payload?.detail) message = payload.detail;
78+
} catch {
79+
/* ignore parse error */
80+
}
81+
throw new Error(message);
82+
}
83+
84+
export async function getProviders(): Promise<AiProviderCatalog> {
85+
const res = await fetch(apiUrl("/api/ai/providers"));
86+
return parseJson(res, "Failed to load AI providers.");
87+
}
88+
89+
export async function getProfile(): Promise<AiProfilePayload> {
90+
const res = await fetch(apiUrl("/api/ai/profile"));
91+
return parseJson(res, "Failed to load AI profile.");
92+
}
93+
94+
export async function updateProfile(payload: {
95+
provider?: string;
96+
model?: string;
97+
role?: string;
98+
baseUrl?: string;
99+
temperature?: number;
100+
maxTokens?: number;
101+
systemPrompt?: string;
102+
}): Promise<AiProfilePayload & { revision: number }> {
103+
const res = await fetch(apiUrl("/api/ai/profile"), {
104+
method: "PUT",
105+
headers: { "Content-Type": "application/json" },
106+
body: JSON.stringify(payload),
107+
});
108+
return parseJson(res, "Failed to update AI profile.");
109+
}
110+
111+
export async function saveSecret(provider: string, apiKey: string): Promise<unknown> {
112+
const res = await fetch(apiUrl("/api/ai/profile/secrets"), {
113+
method: "POST",
114+
headers: { "Content-Type": "application/json" },
115+
body: JSON.stringify({ provider, apiKey }),
116+
});
117+
return parseJson(res, "Failed to save secret.");
118+
}
119+
120+
export async function clearSecret(provider: string): Promise<unknown> {
121+
const res = await fetch(apiUrl("/api/ai/profile/secrets"), {
122+
method: "POST",
123+
headers: { "Content-Type": "application/json" },
124+
body: JSON.stringify({ provider, clear: true }),
125+
});
126+
return parseJson(res, "Failed to clear secret.");
127+
}
128+
129+
export async function validateProvider(
130+
provider: string,
131+
model?: string,
132+
): Promise<{ valid: boolean; model?: string; error?: string }> {
133+
const params = new URLSearchParams({ provider });
134+
if (model) params.set("model", model);
135+
const res = await fetch(apiUrl(`/api/ai/provider/validate?${params}`), { method: "POST" });
136+
return parseJson(res, "Failed to validate provider.");
137+
}
138+
139+
export async function getModels(provider: string): Promise<{ models: string[] }> {
140+
const res = await fetch(apiUrl(`/api/ai/models/${encodeURIComponent(provider)}`));
141+
return parseJson(res, "Failed to load models.");
142+
}
143+
144+
export async function createConversation(
145+
role: string = "copilot",
146+
systemPrompt?: string,
147+
): Promise<AiConversation> {
148+
const params = new URLSearchParams({ role });
149+
if (systemPrompt) params.set("systemPrompt", systemPrompt);
150+
const res = await fetch(apiUrl(`/api/ai/conversations?${params}`), { method: "POST" });
151+
return parseJson(res, "Failed to create conversation.");
152+
}
153+
154+
export async function listConversations(): Promise<{ conversations: AiConversation[] }> {
155+
const res = await fetch(apiUrl("/api/ai/conversations"));
156+
return parseJson(res, "Failed to list conversations.");
157+
}
158+
159+
export async function deleteConversation(conversationId: string): Promise<{ ok: boolean }> {
160+
const res = await fetch(apiUrl(`/api/ai/conversations/${encodeURIComponent(conversationId)}`), {
161+
method: "DELETE",
162+
});
163+
return parseJson(res, "Failed to delete conversation.");
164+
}
165+
166+
export async function sendChatMessage(payload: {
167+
conversationId?: string;
168+
message: string;
169+
sessionId?: string;
170+
provider?: string;
171+
role?: string;
172+
}): Promise<AiChatResponse> {
173+
const res = await fetch(apiUrl("/api/ai/chat"), {
174+
method: "POST",
175+
headers: { "Content-Type": "application/json" },
176+
body: JSON.stringify(payload),
177+
});
178+
return parseJson(res, "Failed to send chat message.");
179+
}
180+
181+
export async function oauthAuthorize(): Promise<OAuthAuthorizeResponse> {
182+
const res = await fetch(apiUrl("/api/oauth/authorize"));
183+
return parseJson(res, "Failed to start OAuth flow.");
184+
}
185+
186+
export async function oauthStatus(): Promise<OAuthStatusResponse> {
187+
const res = await fetch(apiUrl("/api/oauth/status"));
188+
return parseJson(res, "OAuth status check failed.");
189+
}
190+
191+
export async function oauthLogout(): Promise<{ ok: boolean }> {
192+
const res = await fetch(apiUrl("/api/oauth/logout"), { method: "POST" });
193+
return parseJson(res, "Failed to logout.");
194+
}
195+
196+
export function subscribeProfileEvents(onUpdate: (profile: AiProfilePayload) => void): () => void {
197+
const es = new EventSource(apiUrl("/api/ai/profile/events"));
198+
es.addEventListener("profile_changed", (e) => {
199+
try {
200+
const data = JSON.parse(e.data) as AiProfilePayload;
201+
onUpdate(data);
202+
} catch {
203+
/* ignore parse error */
204+
}
205+
});
206+
return () => es.close();
207+
}

0 commit comments

Comments
 (0)