@mosaic/mosaic is now the single package providing both: - 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.) - 'mosaic-wizard' binary (installation wizard) Changes: - Move packages/cli/src/* into packages/mosaic/src/ - Convert dynamic @mosaic/mosaic imports to static relative imports - Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic - Add jsx: react-jsx to mosaic's tsconfig - Exclude packages/cli from workspace (pnpm-workspace.yaml) - Update install.sh to install @mosaic/mosaic instead of @mosaic/cli - Bump version to 0.0.17 This eliminates the circular dependency between @mosaic/cli and @mosaic/mosaic that was blocking the build graph.
144 lines
4.0 KiB
TypeScript
144 lines
4.0 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
export interface ConversationSummary {
|
|
id: string;
|
|
title: string | null;
|
|
archived: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface UseConversationsOptions {
|
|
gatewayUrl: string;
|
|
sessionCookie?: string;
|
|
}
|
|
|
|
export interface UseConversationsReturn {
|
|
conversations: ConversationSummary[];
|
|
loading: boolean;
|
|
error: string | null;
|
|
refresh: () => Promise<void>;
|
|
createConversation: (title?: string) => Promise<ConversationSummary | null>;
|
|
deleteConversation: (id: string) => Promise<boolean>;
|
|
renameConversation: (id: string, title: string) => Promise<boolean>;
|
|
}
|
|
|
|
export function useConversations(opts: UseConversationsOptions): UseConversationsReturn {
|
|
const { gatewayUrl, sessionCookie } = opts;
|
|
|
|
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const mountedRef = useRef(true);
|
|
|
|
const headers = useCallback(
|
|
(includeContentType = true): Record<string, string> => {
|
|
const h: Record<string, string> = { Origin: gatewayUrl };
|
|
if (includeContentType) h['Content-Type'] = 'application/json';
|
|
if (sessionCookie) h['Cookie'] = sessionCookie;
|
|
return h;
|
|
},
|
|
[gatewayUrl, sessionCookie],
|
|
);
|
|
|
|
const refresh = useCallback(async () => {
|
|
if (!mountedRef.current) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch(`${gatewayUrl}/api/conversations`, { headers: headers(false) });
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = (await res.json()) as ConversationSummary[];
|
|
if (mountedRef.current) {
|
|
setConversations(data);
|
|
}
|
|
} catch (err) {
|
|
if (mountedRef.current) {
|
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
}
|
|
} finally {
|
|
if (mountedRef.current) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
}, [gatewayUrl, headers]);
|
|
|
|
useEffect(() => {
|
|
mountedRef.current = true;
|
|
void refresh();
|
|
return () => {
|
|
mountedRef.current = false;
|
|
};
|
|
}, [refresh]);
|
|
|
|
const createConversation = useCallback(
|
|
async (title?: string): Promise<ConversationSummary | null> => {
|
|
try {
|
|
const res = await fetch(`${gatewayUrl}/api/conversations`, {
|
|
method: 'POST',
|
|
headers: headers(),
|
|
body: JSON.stringify({ title: title ?? null }),
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = (await res.json()) as ConversationSummary;
|
|
if (mountedRef.current) {
|
|
setConversations((prev) => [data, ...prev]);
|
|
}
|
|
return data;
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
[gatewayUrl, headers],
|
|
);
|
|
|
|
const deleteConversation = useCallback(
|
|
async (id: string): Promise<boolean> => {
|
|
try {
|
|
const res = await fetch(`${gatewayUrl}/api/conversations/${id}`, {
|
|
method: 'DELETE',
|
|
headers: headers(false),
|
|
});
|
|
if (!res.ok) return false;
|
|
if (mountedRef.current) {
|
|
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
[gatewayUrl, headers],
|
|
);
|
|
|
|
const renameConversation = useCallback(
|
|
async (id: string, title: string): Promise<boolean> => {
|
|
try {
|
|
const res = await fetch(`${gatewayUrl}/api/conversations/${id}`, {
|
|
method: 'PATCH',
|
|
headers: headers(),
|
|
body: JSON.stringify({ title }),
|
|
});
|
|
if (!res.ok) return false;
|
|
if (mountedRef.current) {
|
|
setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title } : c)));
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
[gatewayUrl, headers],
|
|
);
|
|
|
|
return {
|
|
conversations,
|
|
loading,
|
|
error,
|
|
refresh,
|
|
createConversation,
|
|
deleteConversation,
|
|
renameConversation,
|
|
};
|
|
}
|