feat: redesign coding default entry
This commit is contained in:
@@ -218,7 +218,7 @@ function ProjectChatRoute() {
|
||||
if (!project) {
|
||||
if (!cancelled) {
|
||||
setResolvedProjectKey(null);
|
||||
setResolvedRoute('config');
|
||||
setResolvedRoute('chat');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ 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 { 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';
|
||||
@@ -393,7 +394,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
}, [authUser, refreshTokenUsage]);
|
||||
|
||||
|
||||
const openCreateProject = () => {
|
||||
const openCreateProject = useCallback(() => {
|
||||
setNewProjectName('');
|
||||
setNewProjectSelectedPath('');
|
||||
setNewProjectDirectoryMode('use-selected-directory');
|
||||
@@ -402,7 +403,12 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
setNewProjectIdentityProjectId('');
|
||||
setCreateProjectError(null);
|
||||
setCreateDialogOpen(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProgrammingModule) return undefined;
|
||||
return subscribeCodingProjectCreationRequest(openCreateProject);
|
||||
}, [isProgrammingModule, openCreateProject]);
|
||||
|
||||
const closeCreateProject = () => {
|
||||
if (creatingProject) return;
|
||||
|
||||
12
src/lib/coding-project-entry.ts
Normal file
12
src/lib/coding-project-entry.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
const CODING_PROJECT_CREATE_REQUEST_EVENT = 'makelore:coding-project-create-request';
|
||||
|
||||
export function requestCodingProjectCreation(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(new Event(CODING_PROJECT_CREATE_REQUEST_EVENT));
|
||||
}
|
||||
|
||||
export function subscribeCodingProjectCreationRequest(listener: () => void): () => void {
|
||||
if (typeof window === 'undefined') return () => undefined;
|
||||
window.addEventListener(CODING_PROJECT_CREATE_REQUEST_EVENT, listener);
|
||||
return () => window.removeEventListener(CODING_PROJECT_CREATE_REQUEST_EVENT, listener);
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Bot,
|
||||
ArrowUp,
|
||||
CircleAlert,
|
||||
FolderKanban,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Settings2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -40,10 +43,12 @@ import { CodingConversationSidebar } from './CodingConversationSidebar';
|
||||
import { CodingConversationHeader } from './CodingConversationHeader';
|
||||
import { CodingConversationTimeline } from './CodingConversationTimeline';
|
||||
import { CodingInteractionPanel } from './CodingInteractionPanel';
|
||||
import { CodingWelcomeHero } from './CodingWelcomeHero';
|
||||
import { createLocalConversationSnapshot } from './coding-chat-snapshot';
|
||||
|
||||
export interface CodingChatPanelProps {
|
||||
navigationDraft?: string;
|
||||
onCreateProject?(): void;
|
||||
onOpenProjectSettings?(): void;
|
||||
}
|
||||
|
||||
@@ -109,6 +114,7 @@ function acceptedPromptCount(
|
||||
|
||||
export function CodingChatPanel({
|
||||
navigationDraft,
|
||||
onCreateProject,
|
||||
onOpenProjectSettings,
|
||||
}: CodingChatPanelProps) {
|
||||
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
|
||||
@@ -592,18 +598,87 @@ export function CodingChatPanel({
|
||||
|
||||
if (!activeProject && workspaceLoadState !== 'loading') {
|
||||
return (
|
||||
<section className="flex min-h-0 flex-1 items-center justify-center bg-background p-6" data-testid="coding-chat-empty-project">
|
||||
<div className="max-w-md text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-surface-subtle shadow-[0_0_0_1px_rgba(0,0,0,0.06)]">
|
||||
<Bot className="h-5 w-5 text-muted-foreground" aria-hidden="true" />
|
||||
<section
|
||||
className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-background px-4 pb-3 pt-4 text-foreground sm:px-6 sm:pb-5"
|
||||
data-testid="coding-chat-empty-project"
|
||||
>
|
||||
<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">
|
||||
{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" />
|
||||
<p className="min-w-0 flex-1 text-pretty" role="alert">{workspaceError}</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="min-h-8 shrink-0 rounded-lg px-2.5 text-destructive"
|
||||
onClick={() => void loadWorkspace().catch(() => undefined)}
|
||||
>
|
||||
<RefreshCw className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
刷新
|
||||
</Button>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-4 text-balance text-lg font-semibold">先选择一个编程项目</h1>
|
||||
<p className="mt-2 text-pretty text-sm leading-6 text-muted-foreground">
|
||||
项目与智能体配置从本地读取,打开输入框不会提前启动 Pi 运行实例。
|
||||
</p>
|
||||
<Button className="mt-5 min-h-10 rounded-xl" onClick={onOpenProjectSettings}>
|
||||
打开项目设置
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import {
|
||||
Bot,
|
||||
BookOpen,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
@@ -64,6 +63,7 @@ import type {
|
||||
SubagentDetailsV1,
|
||||
} from '../../../shared/coding-conversation-contracts';
|
||||
import { CodingAttachmentPreview } from './CodingAttachmentPreview';
|
||||
import { CodingWelcomeHero } from './CodingWelcomeHero';
|
||||
|
||||
const EMPTY_NODES: ConversationNode[] = [];
|
||||
const IDLE_RUN: ConversationRunState = { status: 'idle' };
|
||||
@@ -1610,7 +1610,10 @@ export const CodingConversationTimeline = memo(function CodingConversationTimeli
|
||||
if (element.scrollTop <= 48) loadEarlier();
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-[50rem] flex-col gap-8">
|
||||
<div className={cn(
|
||||
'mx-auto flex w-full max-w-[50rem] flex-col gap-8',
|
||||
nodes.length === 0 && 'min-h-full',
|
||||
)}>
|
||||
{windowStart > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -1632,15 +1635,10 @@ export const CodingConversationTimeline = memo(function CodingConversationTimeli
|
||||
/>
|
||||
))}
|
||||
{nodes.length === 0 && (
|
||||
<div className="mx-auto flex max-w-md flex-col items-center px-6 py-16 text-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-surface-subtle">
|
||||
<Bot className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
|
||||
</div>
|
||||
<h2 className="mt-4 text-balance text-base font-semibold">开始一条对话</h2>
|
||||
<p className="mt-1.5 text-pretty text-sm leading-6 text-muted-foreground">
|
||||
Pi 会连续展示思考和工具进度,处理完成后自动收纳过程并给出结论。
|
||||
</p>
|
||||
</div>
|
||||
<CodingWelcomeHero
|
||||
headingLevel="h2"
|
||||
className="min-h-[18rem] flex-1 px-6 pb-12 pt-6 sm:pb-16"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
29
src/pages/Chat/CodingWelcomeHero.tsx
Normal file
29
src/pages/Chat/CodingWelcomeHero.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import makeloreMark from '@/assets/logo.svg';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function CodingWelcomeHero({
|
||||
className,
|
||||
headingLevel = 'h1',
|
||||
}: {
|
||||
className?: string;
|
||||
headingLevel?: 'h1' | 'h2';
|
||||
}) {
|
||||
const Heading = headingLevel;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="coding-welcome-hero"
|
||||
className={cn('flex flex-col items-center justify-center text-center', className)}
|
||||
>
|
||||
<img
|
||||
src={makeloreMark}
|
||||
alt="麦洛 M 标识"
|
||||
className="h-auto w-12 select-none sm:w-[3.25rem]"
|
||||
draggable={false}
|
||||
/>
|
||||
<Heading className="mt-6 max-w-2xl text-balance text-[clamp(1.65rem,2.45vw,2.1rem)] font-medium leading-[1.28] tracking-[-0.04em] text-foreground">
|
||||
你想让麦洛和你一起构建什么?
|
||||
</Heading>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CodingChatPanel } from './CodingChatPanel';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { requestCodingProjectCreation } from '@/lib/coding-project-entry';
|
||||
|
||||
function getNavigationDraft(state: unknown): string | undefined {
|
||||
if (!state || typeof state !== 'object') return undefined;
|
||||
@@ -24,6 +25,7 @@ export function Chat() {
|
||||
<div data-testid="chat-operation-page" className="flex h-[calc(100%_+_3rem)] min-h-0 -m-6 overflow-hidden bg-background">
|
||||
<CodingChatPanel
|
||||
navigationDraft={navigationDraft}
|
||||
onCreateProject={requestCodingProjectCreation}
|
||||
onOpenProjectSettings={() => navigate('/project-config')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user