Add initial project structure with essential files
- 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.
This commit is contained in:
+323
@@ -0,0 +1,323 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { EntryView } from './components/EntryView';
|
||||
import type { CreateInput } from './components/NewProjectPanel';
|
||||
import { ProjectView } from './components/ProjectView';
|
||||
import { SettingsDialog } from './components/SettingsDialog';
|
||||
import {
|
||||
daemonIsLive,
|
||||
fetchAgents,
|
||||
fetchDesignSystems,
|
||||
fetchSkills,
|
||||
} from './providers/registry';
|
||||
import { navigate, useRoute } from './router';
|
||||
import { loadConfig, saveConfig } from './state/config';
|
||||
import {
|
||||
createProject,
|
||||
deleteProject as deleteProjectApi,
|
||||
listProjects,
|
||||
listTemplates,
|
||||
patchProject,
|
||||
} from './state/projects';
|
||||
import type {
|
||||
AgentInfo,
|
||||
AppConfig,
|
||||
DesignSystemSummary,
|
||||
Project,
|
||||
ProjectTemplate,
|
||||
SkillSummary,
|
||||
} from './types';
|
||||
|
||||
export function App() {
|
||||
const [config, setConfig] = useState<AppConfig>(() => loadConfig());
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [settingsWelcome, setSettingsWelcome] = useState(false);
|
||||
const [daemonLive, setDaemonLive] = useState(false);
|
||||
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
||||
const [skills, setSkills] = useState<SkillSummary[]>([]);
|
||||
const [designSystems, setDesignSystems] = useState<DesignSystemSummary[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [templates, setTemplates] = useState<ProjectTemplate[]>([]);
|
||||
// Goes false once the bootstrap effect has finished its initial round of
|
||||
// fetches. The entry view uses this to show shimmer / skeleton states
|
||||
// instead of an "empty" page that flickers before data lands.
|
||||
const [bootstrapping, setBootstrapping] = useState(true);
|
||||
const route = useRoute();
|
||||
|
||||
// Bootstrap — detect daemon, load pickers, seed sensible defaults.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const alive = await daemonIsLive();
|
||||
if (cancelled) return;
|
||||
setDaemonLive(alive);
|
||||
const [agentList, skillList, dsList, projectList, templateList] =
|
||||
await Promise.all([
|
||||
alive ? fetchAgents() : Promise.resolve([] as AgentInfo[]),
|
||||
alive ? fetchSkills() : Promise.resolve([] as SkillSummary[]),
|
||||
alive
|
||||
? fetchDesignSystems()
|
||||
: Promise.resolve([] as DesignSystemSummary[]),
|
||||
alive ? listProjects() : Promise.resolve([] as Project[]),
|
||||
alive ? listTemplates() : Promise.resolve([] as ProjectTemplate[]),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setAgents(agentList);
|
||||
setSkills(skillList);
|
||||
setDesignSystems(dsList);
|
||||
setProjects(projectList);
|
||||
setTemplates(templateList);
|
||||
|
||||
setConfig((prev) => {
|
||||
const next = { ...prev };
|
||||
if (alive) {
|
||||
if (!next.agentId) {
|
||||
const firstAvailable = agentList.find((a) => a.available);
|
||||
if (firstAvailable) next.agentId = firstAvailable.id;
|
||||
}
|
||||
if (!next.designSystemId && dsList.length > 0) {
|
||||
next.designSystemId = dsList.find((d) => d.id === 'default')?.id
|
||||
?? dsList[0]!.id;
|
||||
}
|
||||
} else {
|
||||
next.mode = 'api';
|
||||
}
|
||||
saveConfig(next);
|
||||
|
||||
// Pop the onboarding modal only on the first run. Once the user has
|
||||
// saved or skipped past it once, we trust their stored config and
|
||||
// let them re-open Settings explicitly via the env pill.
|
||||
if (!next.onboardingCompleted) {
|
||||
setSettingsWelcome(true);
|
||||
setSettingsOpen(true);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setBootstrapping(false);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshProjects = useCallback(async () => {
|
||||
const list = await listProjects();
|
||||
setProjects(list);
|
||||
}, []);
|
||||
|
||||
const refreshTemplates = useCallback(async () => {
|
||||
const list = await listTemplates();
|
||||
setTemplates(list);
|
||||
}, []);
|
||||
|
||||
const handleConfigSave = useCallback((next: AppConfig) => {
|
||||
// Saving from any settings dialog (welcome or regular) counts as
|
||||
// having completed onboarding — the user has actively chosen a
|
||||
// configuration, so future page loads can skip the auto-popup.
|
||||
const withOnboarding: AppConfig = { ...next, onboardingCompleted: true };
|
||||
saveConfig(withOnboarding);
|
||||
setConfig(withOnboarding);
|
||||
}, []);
|
||||
|
||||
const handleModeChange = useCallback(
|
||||
(mode: AppConfig['mode']) => {
|
||||
const next = { ...config, mode };
|
||||
saveConfig(next);
|
||||
setConfig(next);
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
const handleAgentChange = useCallback(
|
||||
(agentId: string) => {
|
||||
const next = { ...config, agentId };
|
||||
saveConfig(next);
|
||||
setConfig(next);
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
const handleChangeDefaultDesignSystem = useCallback(
|
||||
(designSystemId: string) => {
|
||||
const next = { ...config, designSystemId };
|
||||
saveConfig(next);
|
||||
setConfig(next);
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
const refreshAgents = useCallback(async () => {
|
||||
const next = await fetchAgents();
|
||||
setAgents(next);
|
||||
}, []);
|
||||
|
||||
const handleCreateProject = useCallback(
|
||||
async (input: CreateInput & { pendingPrompt?: string }) => {
|
||||
// Honor an explicit `null` design system — the create panel defaults
|
||||
// to "None" for every kind now, and the user expects that to land
|
||||
// as a no-design-system project rather than silently inheriting the
|
||||
// workspace default.
|
||||
const result = await createProject({
|
||||
name: input.name,
|
||||
skillId: input.skillId,
|
||||
designSystemId: input.designSystemId,
|
||||
pendingPrompt: input.pendingPrompt,
|
||||
metadata: input.metadata,
|
||||
});
|
||||
if (!result) return;
|
||||
setProjects((curr) => [result.project, ...curr.filter((p) => p.id !== result.project.id)]);
|
||||
navigate({ kind: 'project', projectId: result.project.id, fileName: null });
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleOpenProject = useCallback((id: string) => {
|
||||
navigate({ kind: 'project', projectId: id, fileName: null });
|
||||
}, []);
|
||||
|
||||
const handleDeleteProject = useCallback(async (id: string) => {
|
||||
const ok = await deleteProjectApi(id);
|
||||
if (!ok) return;
|
||||
setProjects((curr) => curr.filter((p) => p.id !== id));
|
||||
if (route.kind === 'project' && route.projectId === id) {
|
||||
navigate({ kind: 'home' });
|
||||
}
|
||||
}, [route]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
navigate({ kind: 'home' });
|
||||
}, []);
|
||||
|
||||
const handleClearPendingPrompt = useCallback(() => {
|
||||
const projectId =
|
||||
route.kind === 'project' ? route.projectId : null;
|
||||
if (!projectId) return;
|
||||
setProjects((curr) =>
|
||||
curr.map((p) =>
|
||||
p.id === projectId ? { ...p, pendingPrompt: undefined } : p,
|
||||
),
|
||||
);
|
||||
void patchProject(projectId, { pendingPrompt: undefined });
|
||||
}, [route]);
|
||||
|
||||
const handleTouchProject = useCallback(() => {
|
||||
const projectId =
|
||||
route.kind === 'project' ? route.projectId : null;
|
||||
if (!projectId) return;
|
||||
const updatedAt = Date.now();
|
||||
setProjects((curr) =>
|
||||
curr.map((p) => (p.id === projectId ? { ...p, updatedAt } : p)),
|
||||
);
|
||||
void patchProject(projectId, { updatedAt });
|
||||
}, [route]);
|
||||
|
||||
const handleProjectChange = useCallback((updated: Project) => {
|
||||
setProjects((curr) =>
|
||||
curr.map((p) => (p.id === updated.id ? updated : p)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const activeProject =
|
||||
route.kind === 'project'
|
||||
? projects.find((p) => p.id === route.projectId) ?? null
|
||||
: null;
|
||||
|
||||
// Deep-linked route to a project we don't have yet (e.g. after a refresh
|
||||
// that finishes after the project list comes back). Fetch it in the
|
||||
// background so the view can render rather than bouncing to home.
|
||||
useEffect(() => {
|
||||
if (route.kind !== 'project') return;
|
||||
if (activeProject) return;
|
||||
if (!projects.length && !daemonLive) return;
|
||||
if (projects.some((p) => p.id === route.projectId)) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const list = await listProjects();
|
||||
if (cancelled) return;
|
||||
setProjects(list);
|
||||
if (!list.find((p) => p.id === route.projectId)) {
|
||||
navigate({ kind: 'home' }, { replace: true });
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [route, activeProject, projects, daemonLive]);
|
||||
|
||||
const openSettings = useCallback(() => {
|
||||
setSettingsWelcome(false);
|
||||
setSettingsOpen(true);
|
||||
}, []);
|
||||
|
||||
// When the user lands on the entry view (route.kind === 'home'), pull
|
||||
// a fresh template list. The template store is global — if they just
|
||||
// saved a template inside a project, returning home should reflect it
|
||||
// immediately in the From-template tab without forcing a page reload.
|
||||
useEffect(() => {
|
||||
if (route.kind !== 'home') return;
|
||||
void refreshTemplates();
|
||||
}, [route.kind, refreshTemplates]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeProject ? (
|
||||
<ProjectView
|
||||
key={activeProject.id}
|
||||
project={activeProject}
|
||||
routeFileName={route.kind === 'project' ? route.fileName : null}
|
||||
config={config}
|
||||
agents={agents}
|
||||
skills={skills}
|
||||
designSystems={designSystems}
|
||||
daemonLive={daemonLive}
|
||||
onModeChange={handleModeChange}
|
||||
onAgentChange={handleAgentChange}
|
||||
onRefreshAgents={refreshAgents}
|
||||
onOpenSettings={openSettings}
|
||||
onBack={handleBack}
|
||||
onClearPendingPrompt={handleClearPendingPrompt}
|
||||
onTouchProject={handleTouchProject}
|
||||
onProjectChange={handleProjectChange}
|
||||
onProjectsRefresh={refreshProjects}
|
||||
/>
|
||||
) : (
|
||||
<EntryView
|
||||
skills={skills}
|
||||
designSystems={designSystems}
|
||||
projects={projects}
|
||||
templates={templates}
|
||||
defaultDesignSystemId={config.designSystemId}
|
||||
config={config}
|
||||
agents={agents}
|
||||
loading={bootstrapping}
|
||||
onCreateProject={handleCreateProject}
|
||||
onOpenProject={handleOpenProject}
|
||||
onDeleteProject={handleDeleteProject}
|
||||
onChangeDefaultDesignSystem={handleChangeDefaultDesignSystem}
|
||||
onOpenSettings={openSettings}
|
||||
/>
|
||||
)}
|
||||
{settingsOpen ? (
|
||||
<SettingsDialog
|
||||
initial={config}
|
||||
agents={agents}
|
||||
daemonLive={daemonLive}
|
||||
welcome={settingsWelcome}
|
||||
onSave={handleConfigSave}
|
||||
onClose={() => {
|
||||
// Dismissing the welcome modal (Skip for now / backdrop click)
|
||||
// also counts as onboarding-done; we don't want to keep
|
||||
// re-prompting on every refresh just because the user opted
|
||||
// not to save.
|
||||
if (settingsWelcome && !config.onboardingCompleted) {
|
||||
const next: AppConfig = { ...config, onboardingCompleted: true };
|
||||
saveConfig(next);
|
||||
setConfig(next);
|
||||
}
|
||||
setSettingsOpen(false);
|
||||
}}
|
||||
onRefreshAgents={refreshAgents}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user