reorganize prop and state
This commit is contained in:
@@ -18,7 +18,8 @@
|
||||
"lucide-react": "^0.546.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"vite": "^6.2.0"
|
||||
"vite": "^6.2.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
|
||||
@@ -28,11 +28,16 @@ import {
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import HomeView, {
|
||||
type HomeDraft,
|
||||
type HomeAgentMode,
|
||||
type HomeAgentModeItem,
|
||||
type HomeAttachmentDraft,
|
||||
type HomeShowcaseResource,
|
||||
} from './view/home';
|
||||
import { useLauncherHomeDraftStore } from './stores/useHomeDraftStore';
|
||||
import {
|
||||
getClientProfileDashboard,
|
||||
getClientProfileWalletLedger,
|
||||
} from './services/clientApi';
|
||||
|
||||
import type {
|
||||
AuthEntryResponse,
|
||||
@@ -51,7 +56,6 @@ import {
|
||||
import type {
|
||||
ProfileDashboardSummary,
|
||||
ProfileWalletLedgerEntry,
|
||||
ProfileWalletLedgerResponse,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
createGameCreationAppManifest,
|
||||
@@ -144,26 +148,6 @@ type PendingNonEmptyProject =
|
||||
projectPath: string;
|
||||
};
|
||||
|
||||
type EditorShowcaseResource = HomeShowcaseResource & {
|
||||
resourceId: string;
|
||||
showcaseId?: string | null;
|
||||
label?: string | null;
|
||||
imageSrc: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
prompt?: string | null;
|
||||
actualPrompt?: string | null;
|
||||
assetKind?: string | null;
|
||||
authorDisplayName?: string | null;
|
||||
authorPublicUserCode?: string | null;
|
||||
likeCount?: number | null;
|
||||
};
|
||||
|
||||
type EditorShowcaseResourceListResponse = {
|
||||
resources: EditorShowcaseResource[];
|
||||
nextCursor?: string | null;
|
||||
};
|
||||
|
||||
interface InitLocalProjectResult {
|
||||
projectPath: string;
|
||||
manifestPath: string;
|
||||
@@ -1237,31 +1221,6 @@ async function logoutClientAuthSession() {
|
||||
}
|
||||
}
|
||||
|
||||
function getClientProfileDashboard() {
|
||||
return requestAuthJson<ProfileDashboardSummary>(
|
||||
'/api/profile/dashboard',
|
||||
{ method: 'GET' },
|
||||
'读取泥点余额失败',
|
||||
);
|
||||
}
|
||||
|
||||
function getClientProfileWalletLedger() {
|
||||
return requestAuthJson<ProfileWalletLedgerResponse>(
|
||||
'/api/profile/wallet-ledger',
|
||||
{ method: 'GET' },
|
||||
'读取泥点明细失败',
|
||||
);
|
||||
}
|
||||
|
||||
function listClientShowcaseResources() {
|
||||
return requestAuthJson<EditorShowcaseResourceListResponse>(
|
||||
'/api/editor/showcase/resources',
|
||||
{ method: 'GET' },
|
||||
'读取灵感推荐失败',
|
||||
{ skipAuth: true },
|
||||
);
|
||||
}
|
||||
|
||||
const homeAgentModeItems: HomeAgentModeItem[] = [
|
||||
{
|
||||
mode: 'game',
|
||||
@@ -1320,10 +1279,6 @@ function formatLocalDateTime(value: string | number | null | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
function createHomeAttachmentId(file: File, index: number) {
|
||||
return `${file.name}:${file.size}:${file.lastModified}:${index}:${Date.now()}`;
|
||||
}
|
||||
|
||||
function buildHomeConversationContent(
|
||||
mode: HomeAgentMode,
|
||||
prompt: string,
|
||||
@@ -2153,12 +2108,6 @@ export function WorkspaceLauncher({
|
||||
const [projectPath, setProjectPath] = useState(defaultProjectPath);
|
||||
const [status, setStatus] = useState('请选择项目');
|
||||
const [launcherView, setLauncherView] = useState<LauncherView>(initialView);
|
||||
const [homeAgentMode, setHomeAgentMode] = useState<HomeAgentMode>('game');
|
||||
const [homePrompt, setHomePrompt] = useState('');
|
||||
const [homeAttachments, setHomeAttachments] = useState<HomeAttachmentDraft[]>(
|
||||
[],
|
||||
);
|
||||
const [homeCreationBusy, setHomeCreationBusy] = useState(false);
|
||||
const [currentProjectContext, setCurrentProjectContext] =
|
||||
useState<LauncherProjectContext | null>(null);
|
||||
const [recentWorkspaces, setRecentWorkspaces] =
|
||||
@@ -2186,13 +2135,11 @@ export function WorkspaceLauncher({
|
||||
title: string;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
const [showcaseResources, setShowcaseResources] = useState<
|
||||
EditorShowcaseResource[]
|
||||
>([]);
|
||||
const [showcaseStatus, setShowcaseStatus] = useState('正在读取灵感');
|
||||
const homeAttachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const accountMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const helpMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const resetLauncherHomeDraft = useLauncherHomeDraftStore(
|
||||
(state) => state.reset,
|
||||
);
|
||||
const launcherAgentChatAgents = deriveAgentStatusCards(seedManifest, null);
|
||||
const [agentChatProjectPath, setAgentChatProjectPath] =
|
||||
useState(defaultProjectPath);
|
||||
@@ -2258,23 +2205,8 @@ export function WorkspaceLauncher({
|
||||
return;
|
||||
}
|
||||
setProfileDashboard(null);
|
||||
setWalletLedgerStatus(error instanceof Error ? error.message : '泥点读取失败');
|
||||
});
|
||||
void listClientShowcaseResources()
|
||||
.then((response) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setShowcaseResources(response.resources.slice(0, 6));
|
||||
setShowcaseStatus(response.resources.length > 0 ? '已读取灵感' : '暂无灵感');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setShowcaseResources([]);
|
||||
setShowcaseStatus(
|
||||
error instanceof Error ? error.message : '灵感读取失败',
|
||||
setWalletLedgerStatus(
|
||||
error instanceof Error ? error.message : '泥点读取失败',
|
||||
);
|
||||
});
|
||||
return () => {
|
||||
@@ -2433,14 +2365,13 @@ export function WorkspaceLauncher({
|
||||
) {
|
||||
const trimmedProjectPath = validateProjectPath(nextProjectPath);
|
||||
if (!trimmedProjectPath) {
|
||||
return;
|
||||
return '项目目录无效';
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setStatus('需要在 Tauri App 内运行');
|
||||
return;
|
||||
return '需要在 Tauri App 内运行';
|
||||
}
|
||||
setHomeCreationBusy(true);
|
||||
setStatus('正在创建项目');
|
||||
try {
|
||||
if (!skipNonEmptyCheck) {
|
||||
@@ -2457,7 +2388,7 @@ export function WorkspaceLauncher({
|
||||
attachments,
|
||||
});
|
||||
setStatus('目标文件夹不是空的');
|
||||
return;
|
||||
return '目标文件夹不是空的,请确认是否继续新建';
|
||||
}
|
||||
}
|
||||
const result = await invoke<InitLocalProjectResult>(
|
||||
@@ -2503,12 +2434,11 @@ export function WorkspaceLauncher({
|
||||
recentRunStopReason: null,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
setHomePrompt('');
|
||||
setHomeAttachments([]);
|
||||
return '已创建项目并进入项目开发';
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
setHomeCreationBusy(false);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setStatus(message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2649,43 +2579,51 @@ export function WorkspaceLauncher({
|
||||
void openProject(projectPath, 'open');
|
||||
}
|
||||
|
||||
async function handleHomeSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const activeMode = homeAgentModeItems.find(
|
||||
(item) => item.mode === homeAgentMode,
|
||||
);
|
||||
if (!homePrompt.trim() && homeAttachments.length === 0) {
|
||||
setStatus(activeMode?.emptyPrompt ?? '请输入需求或上传附件');
|
||||
return;
|
||||
}
|
||||
async function createHomeDraft(draft: HomeDraft) {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setStatus('需要在 Tauri App 内运行');
|
||||
return;
|
||||
throw new Error('需要在 Tauri App 内运行');
|
||||
}
|
||||
setHomeCreationBusy(true);
|
||||
setStatus('请选择项目目录');
|
||||
const selectedPath = await invoke<string | null>(
|
||||
'pick_local_project_directory',
|
||||
);
|
||||
if (!selectedPath) {
|
||||
return '已取消';
|
||||
}
|
||||
return createHomeProjectFromDirectory(
|
||||
selectedPath,
|
||||
draft.mode,
|
||||
draft.prompt,
|
||||
draft.attachments,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadWalletLedger() {
|
||||
setWalletLedgerStatus('正在读取明细');
|
||||
try {
|
||||
const selectedPath = await invoke<string | null>(
|
||||
'pick_local_project_directory',
|
||||
);
|
||||
if (!selectedPath) {
|
||||
setStatus('已取消');
|
||||
return;
|
||||
}
|
||||
await createHomeProjectFromDirectory(
|
||||
selectedPath,
|
||||
homeAgentMode,
|
||||
homePrompt,
|
||||
homeAttachments,
|
||||
const response = await getClientProfileWalletLedger();
|
||||
setWalletLedger(response.entries.slice(0, 5));
|
||||
setWalletLedgerStatus(
|
||||
response.entries.length > 0 ? '已读取明细' : '暂无泥点明细',
|
||||
);
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
setHomeCreationBusy(false);
|
||||
setWalletLedger([]);
|
||||
setWalletLedgerStatus(
|
||||
error instanceof Error ? error.message : '明细读取失败',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function showLauncherNotice(title: string) {
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
setWalletPanelOpen(false);
|
||||
setLauncherNotice({
|
||||
title,
|
||||
message: `${title}正在接入中,当前版本会先保留入口。`,
|
||||
});
|
||||
}
|
||||
|
||||
async function handlePickProjectDirectory() {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
@@ -2733,53 +2671,6 @@ export function WorkspaceLauncher({
|
||||
}
|
||||
}
|
||||
|
||||
function handleHomeAttachmentSelection(event: ChangeEvent<HTMLInputElement>) {
|
||||
const files = Array.from(event.currentTarget.files ?? []);
|
||||
event.currentTarget.value = '';
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
setHomeAttachments((current) => [
|
||||
...current,
|
||||
...files.map((file, index) => ({
|
||||
id: createHomeAttachmentId(file, index),
|
||||
file,
|
||||
})),
|
||||
]);
|
||||
}
|
||||
|
||||
function removeHomeAttachment(attachmentId: string) {
|
||||
setHomeAttachments((current) =>
|
||||
current.filter((attachment) => attachment.id !== attachmentId),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadWalletLedger() {
|
||||
setWalletLedgerStatus('正在读取明细');
|
||||
try {
|
||||
const response = await getClientProfileWalletLedger();
|
||||
setWalletLedger(response.entries.slice(0, 5));
|
||||
setWalletLedgerStatus(
|
||||
response.entries.length > 0 ? '已读取明细' : '暂无泥点明细',
|
||||
);
|
||||
} catch (error) {
|
||||
setWalletLedger([]);
|
||||
setWalletLedgerStatus(
|
||||
error instanceof Error ? error.message : '明细读取失败',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function showLauncherNotice(title: string) {
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
setWalletPanelOpen(false);
|
||||
setLauncherNotice({
|
||||
title,
|
||||
message: `${title}正在接入中,当前版本会先保留入口。`,
|
||||
});
|
||||
}
|
||||
|
||||
function handleRecentWorkspaceRefresh() {
|
||||
if (recentWorkspaces.length === 0 || recentWorkspaceRefreshing) {
|
||||
return;
|
||||
@@ -3241,7 +3132,9 @@ export function WorkspaceLauncher({
|
||||
<div className="launcher-account-popover" role="menu">
|
||||
<strong>{currentUser.displayName || '陶泥儿用户'}</strong>
|
||||
<span>
|
||||
{currentUser.phoneNumberMasked || currentUser.publicUserCode}
|
||||
{currentUser.phoneNumberMasked ||
|
||||
currentUser.publicUserCode ||
|
||||
'账号信息读取中'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -3264,7 +3157,14 @@ export function WorkspaceLauncher({
|
||||
>
|
||||
反馈联系
|
||||
</button>
|
||||
<button type="button" role="menuitem" onClick={onLogout}>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
resetLauncherHomeDraft();
|
||||
onLogout();
|
||||
}}
|
||||
>
|
||||
<LogOut size={14} aria-hidden="true" />
|
||||
退出登录
|
||||
</button>
|
||||
@@ -3394,21 +3294,9 @@ export function WorkspaceLauncher({
|
||||
{launcherView === 'home' ? (
|
||||
<HomeView
|
||||
hasPromo={launcherNotifications.length > 0}
|
||||
homeAgentMode={homeAgentMode}
|
||||
homeAgentModeItems={homeAgentModeItems}
|
||||
homePrompt={homePrompt}
|
||||
homeAttachments={homeAttachments}
|
||||
homeAttachmentInputRef={homeAttachmentInputRef}
|
||||
homeCreationBusy={homeCreationBusy}
|
||||
status={status}
|
||||
recentProjectRows={recentProjectRows}
|
||||
showcaseResources={showcaseResources}
|
||||
showcaseStatus={showcaseStatus}
|
||||
onHomeAgentModeChange={setHomeAgentMode}
|
||||
onHomePromptChange={setHomePrompt}
|
||||
onHomeAttachmentSelection={handleHomeAttachmentSelection}
|
||||
onHomeAttachmentRemove={removeHomeAttachment}
|
||||
onHomeSubmit={handleHomeSubmit}
|
||||
onCreateDraft={createHomeDraft}
|
||||
onProjectsOpen={() => setLauncherView('projects')}
|
||||
onProjectOpen={(path) => {
|
||||
setProjectPath(path);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
API_RESPONSE_ENVELOPE_HEADER,
|
||||
API_RESPONSE_ENVELOPE_VERSION,
|
||||
ProfileDashboardSummary,
|
||||
ProfileWalletLedgerResponse,
|
||||
unwrapApiResponse,
|
||||
} from '../../../../packages/shared/src';
|
||||
|
||||
const ACCESS_TOKEN_STORAGE_KEY = 'genarrative.auth.access-token.v1';
|
||||
const DEFAULT_CLIENT_AUTH_API_BASE_URL = 'http://127.0.0.1:8082';
|
||||
|
||||
export type EditorShowcaseResource = {
|
||||
resourceId: string;
|
||||
showcaseId?: string | null;
|
||||
label?: string | null;
|
||||
imageSrc: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
prompt?: string | null;
|
||||
actualPrompt?: string | null;
|
||||
assetKind?: string | null;
|
||||
authorDisplayName?: string | null;
|
||||
authorPublicUserCode?: string | null;
|
||||
likeCount?: number | null;
|
||||
};
|
||||
|
||||
type EditorShowcaseResourceListResponse = {
|
||||
resources: EditorShowcaseResource[];
|
||||
nextCursor?: string | null;
|
||||
};
|
||||
|
||||
export class ClientAuthRequestError extends Error {
|
||||
readonly status: number | null;
|
||||
readonly networkError: boolean;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
options: { status?: number | null; networkError?: boolean } = {},
|
||||
) {
|
||||
super(message);
|
||||
this.status = options.status ?? null;
|
||||
this.networkError = options.networkError ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getStoredAuthAccessToken() {
|
||||
return window.localStorage.getItem(ACCESS_TOKEN_STORAGE_KEY)?.trim() || '';
|
||||
}
|
||||
|
||||
export function setStoredAuthAccessToken(token: string) {
|
||||
const nextToken = token.trim();
|
||||
if (nextToken) {
|
||||
window.localStorage.setItem(ACCESS_TOKEN_STORAGE_KEY, nextToken);
|
||||
return;
|
||||
}
|
||||
window.localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function clearStoredAuthAccessToken() {
|
||||
window.localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
function resolveClientApiUrl(url: string) {
|
||||
if (/^https?:\/\//iu.test(url)) {
|
||||
return url;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
return url;
|
||||
}
|
||||
const isHttpPage =
|
||||
window.location.protocol === 'http:' ||
|
||||
window.location.protocol === 'https:';
|
||||
if (!window.__TAURI__ && isHttpPage) {
|
||||
return url;
|
||||
}
|
||||
return `${DEFAULT_CLIENT_AUTH_API_BASE_URL}${url}`;
|
||||
}
|
||||
|
||||
async function readApiErrorMessage(response: Response, fallback: string) {
|
||||
const text = await response.text();
|
||||
if (!text.trim()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
unwrapApiResponse(JSON.parse(text) as unknown);
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export async function requestClientApi<T>(
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
fallbackMessage: string,
|
||||
options: { skipAuth?: boolean } = {},
|
||||
) {
|
||||
const headers = new Headers(init.headers);
|
||||
headers.set(API_RESPONSE_ENVELOPE_HEADER, API_RESPONSE_ENVELOPE_VERSION);
|
||||
if (!options.skipAuth) {
|
||||
const token = getStoredAuthAccessToken();
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
}
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(resolveClientApiUrl(url), {
|
||||
...init,
|
||||
credentials: 'same-origin',
|
||||
headers,
|
||||
});
|
||||
} catch {
|
||||
throw new ClientAuthRequestError(
|
||||
'无法连接登录服务,请确认配套后端或 API 代理已启动后重试',
|
||||
{ networkError: true },
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new ClientAuthRequestError(
|
||||
await readApiErrorMessage(response, fallbackMessage),
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
const text = await response.text();
|
||||
return text ? unwrapApiResponse<T>(JSON.parse(text) as T) : (null as T);
|
||||
}
|
||||
|
||||
export function getClientProfileDashboard() {
|
||||
return requestClientApi<ProfileDashboardSummary>(
|
||||
'/api/profile/dashboard',
|
||||
{ method: 'GET' },
|
||||
'读取泥点余额失败',
|
||||
);
|
||||
}
|
||||
|
||||
export function getClientProfileWalletLedger() {
|
||||
return requestClientApi<ProfileWalletLedgerResponse>(
|
||||
'/api/profile/wallet-ledger',
|
||||
{ method: 'GET' },
|
||||
'读取泥点明细失败',
|
||||
);
|
||||
}
|
||||
|
||||
export function listClientShowcaseResources() {
|
||||
return requestClientApi<EditorShowcaseResourceListResponse>(
|
||||
'/api/editor/showcase/resources',
|
||||
{ method: 'GET' },
|
||||
'读取灵感推荐失败',
|
||||
{ skipAuth: true },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export type HomeAgentMode = 'game' | 'art' | 'doc';
|
||||
|
||||
export type HomeAttachmentDraft = {
|
||||
id: string;
|
||||
file: File;
|
||||
};
|
||||
|
||||
export type HomeDraft = {
|
||||
mode: HomeAgentMode;
|
||||
prompt: string;
|
||||
attachments: HomeAttachmentDraft[];
|
||||
};
|
||||
|
||||
type UseHomeDraftStore = HomeDraft & {
|
||||
setMode: (mode: HomeAgentMode) => void;
|
||||
setPrompt: (prompt: string) => void;
|
||||
addAttachments: (attachments: HomeAttachmentDraft[]) => void;
|
||||
removeAttachment: (attachmentId: string) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
const initialHomeDraft: HomeDraft = {
|
||||
mode: 'game',
|
||||
prompt: '',
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
// Keep non-serializable File objects available while the Home view is unmounted.
|
||||
export const useLauncherHomeDraftStore = create<UseHomeDraftStore>((set) => ({
|
||||
...initialHomeDraft,
|
||||
setMode: (mode) => set({ mode }),
|
||||
setPrompt: (prompt) => set({ prompt }),
|
||||
addAttachments: (attachments) =>
|
||||
set((state) => ({ attachments: [...state.attachments, ...attachments] })),
|
||||
removeAttachment: (attachmentId) =>
|
||||
set((state) => ({
|
||||
attachments: state.attachments.filter(
|
||||
(attachment) => attachment.id !== attachmentId,
|
||||
),
|
||||
})),
|
||||
reset: () => set(initialHomeDraft),
|
||||
}));
|
||||
@@ -7,17 +7,23 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import type {
|
||||
ChangeEventHandler,
|
||||
FormEventHandler,
|
||||
RefObject,
|
||||
FormEvent,
|
||||
} from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
export type HomeAgentMode = 'game' | 'art' | 'doc';
|
||||
import {
|
||||
useLauncherHomeDraftStore,
|
||||
type HomeAgentMode,
|
||||
type HomeAttachmentDraft,
|
||||
type HomeDraft,
|
||||
} from '../../stores/useHomeDraftStore';
|
||||
import { useHomeShowcase } from './useHomeShowcase';
|
||||
|
||||
export type HomeAttachmentDraft = {
|
||||
id: string;
|
||||
file: File;
|
||||
};
|
||||
export type {
|
||||
HomeAgentMode,
|
||||
HomeAttachmentDraft,
|
||||
HomeDraft,
|
||||
} from '../../stores/useHomeDraftStore';
|
||||
|
||||
export type HomeAgentModeItem = {
|
||||
mode: HomeAgentMode;
|
||||
@@ -46,45 +52,39 @@ export type HomeShowcaseResource = {
|
||||
|
||||
type HomeViewProps = {
|
||||
hasPromo: boolean;
|
||||
homeAgentMode: HomeAgentMode;
|
||||
homeAgentModeItems: readonly HomeAgentModeItem[];
|
||||
homePrompt: string;
|
||||
homeAttachments: readonly HomeAttachmentDraft[];
|
||||
homeAttachmentInputRef: RefObject<HTMLInputElement | null>;
|
||||
homeCreationBusy: boolean;
|
||||
status: string;
|
||||
recentProjectRows: readonly HomeProjectRow[];
|
||||
showcaseResources: readonly HomeShowcaseResource[];
|
||||
showcaseStatus: string;
|
||||
onHomeAgentModeChange: (mode: HomeAgentMode) => void;
|
||||
onHomePromptChange: (value: string) => void;
|
||||
onHomeAttachmentSelection: ChangeEventHandler<HTMLInputElement>;
|
||||
onHomeAttachmentRemove: (attachmentId: string) => void;
|
||||
onHomeSubmit: FormEventHandler<HTMLFormElement>;
|
||||
onCreateDraft: (draft: HomeDraft) => Promise<string>;
|
||||
onProjectsOpen: () => void;
|
||||
onProjectOpen: (path: string) => void;
|
||||
};
|
||||
|
||||
export default function HomeView({
|
||||
hasPromo,
|
||||
homeAgentMode,
|
||||
homeAgentModeItems,
|
||||
homePrompt,
|
||||
homeAttachments,
|
||||
homeAttachmentInputRef,
|
||||
homeCreationBusy,
|
||||
status,
|
||||
recentProjectRows,
|
||||
showcaseResources,
|
||||
showcaseStatus,
|
||||
onHomeAgentModeChange,
|
||||
onHomePromptChange,
|
||||
onHomeAttachmentSelection,
|
||||
onHomeAttachmentRemove,
|
||||
onHomeSubmit,
|
||||
onCreateDraft,
|
||||
onProjectsOpen,
|
||||
onProjectOpen,
|
||||
}: HomeViewProps) {
|
||||
const homeAgentMode = useLauncherHomeDraftStore((state) => state.mode);
|
||||
const homePrompt = useLauncherHomeDraftStore((state) => state.prompt);
|
||||
const homeAttachments = useLauncherHomeDraftStore(
|
||||
(state) => state.attachments,
|
||||
);
|
||||
const setHomeAgentMode = useLauncherHomeDraftStore((state) => state.setMode);
|
||||
const setHomePrompt = useLauncherHomeDraftStore((state) => state.setPrompt);
|
||||
const addHomeAttachments = useLauncherHomeDraftStore(
|
||||
(state) => state.addAttachments,
|
||||
);
|
||||
const removeHomeAttachment = useLauncherHomeDraftStore(
|
||||
(state) => state.removeAttachment,
|
||||
);
|
||||
const [homeCreationBusy, setHomeCreationBusy] = useState(false);
|
||||
const [homeStatus, setHomeStatus] = useState('请选择创作模式');
|
||||
const { resources: showcaseResources, status: showcaseStatus } =
|
||||
useHomeShowcase();
|
||||
const homeAttachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const activeHomeMode =
|
||||
homeAgentModeItems.find((item) => item.mode === homeAgentMode) ??
|
||||
homeAgentModeItems[0];
|
||||
@@ -95,6 +95,48 @@ export default function HomeView({
|
||||
|
||||
const ActiveHomeModeIcon = activeHomeMode.icon;
|
||||
|
||||
function handleHomeAttachmentSelection(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) {
|
||||
const files = Array.from(event.currentTarget.files ?? []);
|
||||
event.currentTarget.value = '';
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
addHomeAttachments(
|
||||
files.map((file, index) => ({
|
||||
id: `${file.name}:${file.size}:${file.lastModified}:${index}:${Date.now()}`,
|
||||
file,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleHomeSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!homePrompt.trim() && homeAttachments.length === 0) {
|
||||
setHomeStatus(
|
||||
homeAgentModeItems.find((item) => item.mode === homeAgentMode)
|
||||
?.emptyPrompt ?? '请输入需求或上传附件',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setHomeCreationBusy(true);
|
||||
setHomeStatus('请选择项目目录');
|
||||
try {
|
||||
setHomeStatus(
|
||||
await onCreateDraft({
|
||||
mode: homeAgentMode,
|
||||
prompt: homePrompt,
|
||||
attachments: homeAttachments,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
setHomeStatus(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
setHomeCreationBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
@@ -132,7 +174,7 @@ export default function HomeView({
|
||||
? 'border-violet-500 bg-[#f8f2ff] text-violet-600'
|
||||
: 'border-[#d6d6d6] bg-white text-[#333]'
|
||||
}`}
|
||||
onClick={() => onHomeAgentModeChange(item.mode)}
|
||||
onClick={() => setHomeAgentMode(item.mode)}
|
||||
>
|
||||
<ModeIcon size={14} aria-hidden="true" />
|
||||
{item.label}
|
||||
@@ -142,14 +184,14 @@ export default function HomeView({
|
||||
</div>
|
||||
<form
|
||||
className="grid min-h-20 w-[min(488px,calc(100vw-110px))] grid-rows-[minmax(42px,auto)_auto_auto] rounded-[18px] border border-[#e1e4e8] bg-white px-3 pb-2 pt-[13px] shadow-[0_8px_24px_rgb(15_23_42_/_8%)] max-[720px]:w-[min(100%,calc(100vw-76px))]"
|
||||
onSubmit={onHomeSubmit}
|
||||
onSubmit={handleHomeSubmit}
|
||||
>
|
||||
<textarea
|
||||
className="min-h-9 w-full resize-none border-0 bg-transparent p-0 text-[13px] text-[#171717] outline-0 placeholder:text-[#bababa]"
|
||||
aria-label="创作想法"
|
||||
placeholder={activeHomeMode.placeholder}
|
||||
value={homePrompt}
|
||||
onChange={(event) => onHomePromptChange(event.currentTarget.value)}
|
||||
onChange={(event) => setHomePrompt(event.currentTarget.value)}
|
||||
/>
|
||||
{homeAttachments.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5 py-2 pb-1" aria-label="附件队列">
|
||||
@@ -163,7 +205,7 @@ export default function HomeView({
|
||||
className="size-[18px] cursor-pointer border-0 bg-transparent p-0 text-gray-500"
|
||||
type="button"
|
||||
aria-label={`移除附件 ${attachment.file.name}`}
|
||||
onClick={() => onHomeAttachmentRemove(attachment.id)}
|
||||
onClick={() => removeHomeAttachment(attachment.id)}
|
||||
>
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
@@ -177,7 +219,7 @@ export default function HomeView({
|
||||
className="pointer-events-none absolute size-px opacity-0"
|
||||
type="file"
|
||||
multiple
|
||||
onChange={onHomeAttachmentSelection}
|
||||
onChange={handleHomeAttachmentSelection}
|
||||
/>
|
||||
<button
|
||||
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[#666]"
|
||||
@@ -187,7 +229,7 @@ export default function HomeView({
|
||||
>
|
||||
<Upload size={15} aria-hidden="true" />
|
||||
</button>
|
||||
<span>{status}</span>
|
||||
<span>{homeStatus}</span>
|
||||
<button
|
||||
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-[#f1f1f1] p-0 text-[#9a9a9a] disabled:cursor-not-allowed disabled:opacity-55"
|
||||
type="submit"
|
||||
@@ -207,7 +249,9 @@ export default function HomeView({
|
||||
<header className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="m-0 text-[15px] text-gray-900">最近项目</h2>
|
||||
<p className="mb-0 mt-1 text-[12px] text-[#8b8b8b]">{status}</p>
|
||||
<p className="mb-0 mt-1 text-[12px] text-[#8b8b8b]">
|
||||
{homeStatus}
|
||||
</p>
|
||||
</div>
|
||||
<div className="launcher-project-list-actions">
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
listClientShowcaseResources,
|
||||
type EditorShowcaseResource,
|
||||
} from '../../services/clientApi';
|
||||
|
||||
export function useHomeShowcase() {
|
||||
const [resources, setResources] = useState<EditorShowcaseResource[]>([]);
|
||||
const [status, setStatus] = useState('正在读取灵感');
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
void listClientShowcaseResources()
|
||||
.then((response) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setResources(response.resources.slice(0, 6));
|
||||
setStatus(response.resources.length > 0 ? '已读取灵感' : '暂无灵感');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setResources([]);
|
||||
setStatus(error instanceof Error ? error.message : '灵感读取失败');
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { resources, status };
|
||||
}
|
||||
Reference in New Issue
Block a user