fix: refine coding project landing actions
This commit is contained in:
@@ -31,7 +31,10 @@ import {
|
||||
import { getAiModuleForPath } from '@/lib/ai-modules';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import { getAuthUserDisplayName } from '@/lib/auth-user-display';
|
||||
import { subscribeCodingProjectCreationRequest } from '@/lib/coding-project-entry';
|
||||
import {
|
||||
subscribeCodingProjectCreationRequest,
|
||||
subscribeCodingProjectOpenRequest,
|
||||
} from '@/lib/coding-project-entry';
|
||||
import { WORKS_SQUARE_TOKEN_USAGE_STALE_EVENT } from '@/lib/works-square-usage-events';
|
||||
import { fetchWorksTokenUsage, type WorksTokenUsage } from '@/lib/works-square';
|
||||
import { isWorksTokenUsageExhausted } from '@/lib/works-square-token-usage';
|
||||
@@ -431,11 +434,11 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
setCreateProjectError(null);
|
||||
};
|
||||
|
||||
const showProjectEntryError = (project: CodingProjectSummary, message: string) => {
|
||||
const showProjectEntryError = useCallback((project: CodingProjectSummary, message: string) => {
|
||||
setProjectEntryError({ project, message });
|
||||
};
|
||||
}, []);
|
||||
|
||||
const enterProject = async (project: CodingProjectSummary) => {
|
||||
const enterProject = useCallback(async (project: CodingProjectSummary) => {
|
||||
const result = await loadProjectConfig(project.id);
|
||||
if (result.status !== 'valid' || !result.config) {
|
||||
showProjectEntryError(project, result.status === 'missing' ? '项目缺少必要的配置文件' : result.error ?? '项目配置无效');
|
||||
@@ -445,16 +448,16 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
navigate(result.config.initialized
|
||||
? '/chat'
|
||||
: projectConfigPath);
|
||||
};
|
||||
}, [loadProjectConfig, navigate, setActiveProject, showProjectEntryError]);
|
||||
|
||||
const readAndEnterProject = async (project: CodingProjectSummary) => {
|
||||
const readAndEnterProject = useCallback(async (project: CodingProjectSummary) => {
|
||||
setProjectEntryError(null);
|
||||
try {
|
||||
await enterProject(project);
|
||||
} catch (error) {
|
||||
showProjectEntryError(project, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
}, [enterProject, showProjectEntryError]);
|
||||
|
||||
|
||||
const confirmCreateProject = async () => {
|
||||
@@ -495,9 +498,17 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
}
|
||||
};
|
||||
|
||||
const openProject = async (project: CodingProjectSummary) => {
|
||||
const openProject = useCallback(async (project: CodingProjectSummary) => {
|
||||
await readAndEnterProject(project);
|
||||
};
|
||||
}, [readAndEnterProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProgrammingModule) return undefined;
|
||||
return subscribeCodingProjectOpenRequest((projectId) => {
|
||||
const project = visibleProjects.find((candidate) => candidate.id === projectId);
|
||||
if (project) void openProject(project);
|
||||
});
|
||||
}, [isProgrammingModule, openProject, visibleProjects]);
|
||||
|
||||
const requestRemoveUnavailableProject = () => {
|
||||
if (!projectEntryError) return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const CODING_PROJECT_CREATE_REQUEST_EVENT = 'makelore:coding-project-create-request';
|
||||
const CODING_PROJECT_OPEN_REQUEST_EVENT = 'makelore:coding-project-open-request';
|
||||
|
||||
export function requestCodingProjectCreation(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
@@ -10,3 +11,22 @@ export function subscribeCodingProjectCreationRequest(listener: () => void): ()
|
||||
window.addEventListener(CODING_PROJECT_CREATE_REQUEST_EVENT, listener);
|
||||
return () => window.removeEventListener(CODING_PROJECT_CREATE_REQUEST_EVENT, listener);
|
||||
}
|
||||
|
||||
export function requestCodingProjectOpen(projectId: string): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(new CustomEvent(CODING_PROJECT_OPEN_REQUEST_EVENT, {
|
||||
detail: { projectId },
|
||||
}));
|
||||
}
|
||||
|
||||
export function subscribeCodingProjectOpenRequest(
|
||||
listener: (projectId: string) => void,
|
||||
): () => void {
|
||||
if (typeof window === 'undefined') return () => undefined;
|
||||
const handleRequest = (event: Event) => {
|
||||
const projectId = (event as CustomEvent<{ projectId?: unknown }>).detail?.projectId;
|
||||
if (typeof projectId === 'string' && projectId.trim()) listener(projectId);
|
||||
};
|
||||
window.addEventListener(CODING_PROJECT_OPEN_REQUEST_EVENT, handleRequest);
|
||||
return () => window.removeEventListener(CODING_PROJECT_OPEN_REQUEST_EVENT, handleRequest);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ArrowUp,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
FolderKanban,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Settings2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -36,6 +35,7 @@ import type {
|
||||
} from '@/types/coding-conversation';
|
||||
import type {
|
||||
CodingConversationMetadata,
|
||||
CodingProjectSummary,
|
||||
} from '@/types/coding-project';
|
||||
import { CodingComposer } from './CodingComposer';
|
||||
import { CodingChangesSummary } from './CodingChangesSummary';
|
||||
@@ -49,6 +49,7 @@ import { createLocalConversationSnapshot } from './coding-chat-snapshot';
|
||||
export interface CodingChatPanelProps {
|
||||
navigationDraft?: string;
|
||||
onCreateProject?(): void;
|
||||
onOpenProject?(projectId: string): void;
|
||||
onOpenProjectSettings?(): void;
|
||||
}
|
||||
|
||||
@@ -112,11 +113,24 @@ function acceptedPromptCount(
|
||||
)).length;
|
||||
}
|
||||
|
||||
const PROJECT_DATE_FORMATTER = new Intl.DateTimeFormat('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
function projectLastOpenedLabel(project: CodingProjectSummary): string {
|
||||
const timestamp = Date.parse(project.lastOpenedAt);
|
||||
if (!Number.isFinite(timestamp)) return '本地项目';
|
||||
return `最近打开 ${PROJECT_DATE_FORMATTER.format(timestamp)}`;
|
||||
}
|
||||
|
||||
export function CodingChatPanel({
|
||||
navigationDraft,
|
||||
onCreateProject,
|
||||
onOpenProject,
|
||||
onOpenProjectSettings,
|
||||
}: CodingChatPanelProps) {
|
||||
const projects = useCodingWorkspaceStore((state) => state.projects);
|
||||
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
|
||||
const config = useCodingWorkspaceStore((state) => state.config);
|
||||
const conversations = useCodingWorkspaceStore((state) => state.conversations);
|
||||
@@ -604,7 +618,7 @@ export function CodingChatPanel({
|
||||
>
|
||||
<CodingWelcomeHero className="min-h-[18rem] flex-1 pb-8 pt-6 sm:pb-12" />
|
||||
|
||||
<div className="mx-auto w-full max-w-[46rem] shrink-0">
|
||||
<div className="mx-auto w-full max-w-[52rem] shrink-0 pb-1">
|
||||
{workspaceError ? (
|
||||
<div className="mb-2 flex min-h-10 items-center gap-2 rounded-xl bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
<CircleAlert className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
@@ -621,64 +635,60 @@ export function CodingChatPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="overflow-hidden rounded-[1.35rem] bg-surface-subtle shadow-[0_12px_36px_rgba(35,43,56,0.07)]" data-testid="coding-project-starter">
|
||||
<button
|
||||
type="button"
|
||||
className="focus-ring flex min-h-10 w-full items-center gap-2.5 px-4 py-2 text-left text-xs font-medium text-muted-foreground transition-colors duration-150 hover:bg-surface-tertiary hover:text-foreground disabled:cursor-default disabled:hover:bg-transparent disabled:hover:text-muted-foreground"
|
||||
onClick={onCreateProject}
|
||||
disabled={!onCreateProject}
|
||||
>
|
||||
<FolderKanban className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<span className="font-semibold text-foreground">尚未选择项目</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>本地工作区</span>
|
||||
</button>
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="coding-start-project-button"
|
||||
className="mx-auto flex h-12 min-w-44 rounded-xl border border-brand/20 bg-brand px-7 text-[15px] font-semibold text-primary-foreground shadow-[0_10px_24px_rgba(190,68,24,0.18)] transition-[transform,box-shadow,background-color] duration-200 hover:-translate-y-0.5 hover:bg-brand-hover hover:shadow-[0_14px_30px_rgba(190,68,24,0.22)] active:translate-y-0"
|
||||
onClick={onCreateProject}
|
||||
disabled={!onCreateProject}
|
||||
>
|
||||
<Plus className="mr-2 h-4.5 w-4.5" aria-hidden="true" />
|
||||
新增项目
|
||||
</Button>
|
||||
|
||||
<div className="chat-composer-surface border p-3 sm:p-3.5">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="coding-start-project-button"
|
||||
className="focus-ring block min-h-[4.5rem] w-full rounded-xl px-1 pb-4 pt-1 text-left text-[15px] leading-6 text-muted-foreground transition-colors duration-150 hover:text-foreground disabled:cursor-default disabled:hover:text-muted-foreground"
|
||||
onClick={onCreateProject}
|
||||
disabled={!onCreateProject}
|
||||
>
|
||||
新建或打开一个本地项目,然后告诉麦洛你想构建什么
|
||||
</button>
|
||||
|
||||
<div className="flex min-h-8 items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-8 rounded-lg px-2.5 text-xs font-medium"
|
||||
onClick={onCreateProject}
|
||||
disabled={!onCreateProject}
|
||||
>
|
||||
<Plus className="mr-1.5 h-4 w-4 text-brand" aria-hidden="true" />
|
||||
新建或打开项目
|
||||
</Button>
|
||||
{onOpenProjectSettings ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-8 rounded-lg px-2.5 text-xs font-medium text-muted-foreground"
|
||||
onClick={onOpenProjectSettings}
|
||||
>
|
||||
<Settings2 className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
项目与插件设置
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
className="ml-auto h-8 w-8 shrink-0 rounded-full bg-foreground p-0 text-background shadow-soft hover:bg-foreground/90"
|
||||
aria-label="新建或打开项目"
|
||||
onClick={onCreateProject}
|
||||
disabled={!onCreateProject}
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
{projects.length > 0 ? (
|
||||
<section className="mt-7" aria-labelledby="coding-existing-projects-title">
|
||||
<div className="mb-3 flex items-end justify-between gap-4 px-0.5">
|
||||
<h2 id="coding-existing-projects-title" className="text-sm font-semibold tracking-[-0.01em]">
|
||||
已有项目
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground">选择一个项目继续</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex gap-3 overflow-x-auto pb-2"
|
||||
data-testid="coding-existing-projects"
|
||||
>
|
||||
{projects.map((project) => (
|
||||
<button
|
||||
type="button"
|
||||
key={project.id}
|
||||
data-testid={`coding-existing-project-${project.id}`}
|
||||
aria-label={`进入项目 ${project.name}`}
|
||||
className="motion-press group/project-card flex min-h-[6.75rem] w-64 shrink-0 flex-col justify-between rounded-2xl bg-surface-subtle p-4 text-left shadow-[0_8px_22px_rgba(35,43,56,0.055)] ring-1 ring-foreground/[0.055] transition-[transform,box-shadow,background-color] duration-200 hover:-translate-y-0.5 hover:bg-white hover:shadow-[0_14px_30px_rgba(35,43,56,0.09)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-2"
|
||||
onClick={() => onOpenProject?.(project.id)}
|
||||
disabled={!onOpenProject}
|
||||
>
|
||||
<span className="flex items-start justify-between gap-4">
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-white text-brand shadow-[0_0_0_1px_rgba(35,43,56,0.06)] group-hover/project-card:bg-brand-soft">
|
||||
<FolderKanban className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<ChevronRight className="mt-1 h-4 w-4 text-muted-foreground/55 transition-[transform,color] duration-200 group-hover/project-card:translate-x-0.5 group-hover/project-card:text-foreground" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="mt-4 min-w-0">
|
||||
<span className="block truncate text-sm font-semibold text-foreground">{project.name}</span>
|
||||
<time
|
||||
dateTime={project.lastOpenedAt}
|
||||
className="mt-1 block text-xs text-muted-foreground"
|
||||
>
|
||||
{projectLastOpenedLabel(project)}
|
||||
</time>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { CodingChatPanel } from './CodingChatPanel';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { requestCodingProjectCreation } from '@/lib/coding-project-entry';
|
||||
import {
|
||||
requestCodingProjectCreation,
|
||||
requestCodingProjectOpen,
|
||||
} from '@/lib/coding-project-entry';
|
||||
|
||||
function getNavigationDraft(state: unknown): string | undefined {
|
||||
if (!state || typeof state !== 'object') return undefined;
|
||||
@@ -26,6 +29,7 @@ export function Chat() {
|
||||
<CodingChatPanel
|
||||
navigationDraft={navigationDraft}
|
||||
onCreateProject={requestCodingProjectCreation}
|
||||
onOpenProject={requestCodingProjectOpen}
|
||||
onOpenProjectSettings={() => navigate('/project-config')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user