样式修改
This commit is contained in:
1 parent
c2c44f8d29
commit
03dff2ce77
17 files changed
+577
-58
No files matched your search
@@ -0,0 +1,9 @@
|
|||||||
|
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||||
|
# AI Studio automatically injects this at runtime from user secrets.
|
||||||
|
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||||
|
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||||
|
|
||||||
|
# APP_URL: The URL where this applet is hosted.
|
||||||
|
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||||
|
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||||
|
APP_URL="MY_APP_URL"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Run and deploy your AI Studio app
|
||||||
|
|
||||||
|
This contains everything you need to run your app locally.
|
||||||
|
|
||||||
|
View your app in AI Studio: https://ai.studio/apps/75261c35-a558-4ef8-9501-e8b7f9c98bfb
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
|
**Prerequisites:** Node.js
|
||||||
|
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
`npm install`
|
||||||
|
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||||
|
3. Run the app:
|
||||||
|
`npm run dev`
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>My Google AI Studio App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Gemini Style Assistant",
|
||||||
|
"description": "A clean, modern AI assistant interface inspired by Google Gemini with centered search, suggestion pills, and a minimalist sidebar.",
|
||||||
|
"requestFramePermissions": [],
|
||||||
|
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "react-example",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"clean": "rm -rf dist server.js",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@google/genai": "^1.29.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"lucide-react": "^0.546.0",
|
||||||
|
"react": "^19.0.1",
|
||||||
|
"react-dom": "^19.0.1",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"motion": "^12.23.24"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"@types/express": "^4.17.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import {
|
||||||
|
MessageSquare,
|
||||||
|
Bot,
|
||||||
|
Wrench,
|
||||||
|
Brain,
|
||||||
|
ListChecks,
|
||||||
|
Settings,
|
||||||
|
Menu,
|
||||||
|
Mic,
|
||||||
|
Plus,
|
||||||
|
Sparkles,
|
||||||
|
ArrowRight,
|
||||||
|
User,
|
||||||
|
History,
|
||||||
|
LayoutGrid,
|
||||||
|
Languages,
|
||||||
|
Code2,
|
||||||
|
Music,
|
||||||
|
Database,
|
||||||
|
Search
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ icon: MessageSquare, label: '对话', active: true },
|
||||||
|
{ icon: Bot, label: '智能体' },
|
||||||
|
{ icon: Wrench, label: '技能' },
|
||||||
|
{ icon: Brain, label: '记忆' },
|
||||||
|
{ icon: ListChecks, label: '任务管理' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentChats = [
|
||||||
|
'CM助手初次问候交谈',
|
||||||
|
'Untitled',
|
||||||
|
'Untitled',
|
||||||
|
'Untitled',
|
||||||
|
'帮我写一个500字的文案',
|
||||||
|
'帮我生成一个写论文的实例',
|
||||||
|
];
|
||||||
|
|
||||||
|
const suggestions = [
|
||||||
|
{ icon: Code2, label: 'Build an Android app', color: 'text-green-600', bg: 'bg-green-50' },
|
||||||
|
{ icon: Languages, label: 'Convert text to speech', color: 'text-blue-600', bg: 'bg-blue-50' },
|
||||||
|
{ icon: Music, label: 'Generate music', color: 'text-purple-600', bg: 'bg-purple-50' },
|
||||||
|
{ icon: Database, label: 'Add database and auth', color: 'text-cyan-600', bg: 'bg-cyan-50' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-full bg-white font-sans text-gray-900 overflow-hidden">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<motion.aside
|
||||||
|
initial={false}
|
||||||
|
animate={{ width: sidebarOpen ? 280 : 0, opacity: sidebarOpen ? 1 : 0 }}
|
||||||
|
className="relative bg-gray-50 border-r border-gray-100 flex flex-col h-full"
|
||||||
|
>
|
||||||
|
<div className="p-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 px-2 py-1.5 bg-red-50 text-red-700 rounded-lg cursor-pointer">
|
||||||
|
<Plus size={18} />
|
||||||
|
<span className="font-medium text-sm">新对话</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 overflow-y-auto px-2 space-y-1">
|
||||||
|
<div className="space-y-1 pb-4">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.label}
|
||||||
|
className={`flex items-center gap-3 px-3 py-2 rounded-xl cursor-pointer transition-colors ${
|
||||||
|
item.active ? 'bg-white shadow-sm font-medium' : 'hover:bg-gray-100 text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<item.icon size={18} />
|
||||||
|
<span className="text-sm">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<h3 className="px-3 mb-2 text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||||
|
最近对话
|
||||||
|
</h3>
|
||||||
|
{recentChats.map((chat, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 rounded-lg cursor-pointer truncate"
|
||||||
|
>
|
||||||
|
{chat}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-gray-200">
|
||||||
|
<div className="flex items-center gap-3 px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-xl cursor-pointer">
|
||||||
|
<Settings size={18} />
|
||||||
|
<span className="text-sm">设置和更多</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1 flex flex-col relative h-full overflow-y-auto">
|
||||||
|
{/* Top bar */}
|
||||||
|
<header className="absolute top-0 w-full p-4 flex items-center justify-between z-10">
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg text-gray-500 transition-colors"
|
||||||
|
>
|
||||||
|
<Menu size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button className="p-2 hover:bg-gray-100 rounded-lg text-gray-500 transition-colors">
|
||||||
|
<Settings size={20} />
|
||||||
|
</button>
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gradient-to-tr from-blue-400 to-purple-400 flex items-center justify-center text-white shadow-sm ring-2 ring-white cursor-pointer overflow-hidden">
|
||||||
|
<User size={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Center Canvas */}
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center px-4 max-w-4xl mx-auto w-full pt-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center mb-12 relative"
|
||||||
|
>
|
||||||
|
<div className="absolute -top-16 left-1/2 -translate-x-1/2 opacity-10 blur-2xl pointer-events-none">
|
||||||
|
<Sparkles size={120} className="text-blue-500 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-5xl font-display font-medium tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-500 mb-4 px-4">
|
||||||
|
Build your ideas with Gemini
|
||||||
|
</h1>
|
||||||
|
<Sparkles className="absolute -right-8 top-0 text-blue-200 opacity-50" size={48} />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Search/Input Area */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="w-full max-w-2xl relative group"
|
||||||
|
>
|
||||||
|
<div className="gemini-gradient-border gemini-shadow rounded-3xl bg-white p-2">
|
||||||
|
<div className="flex flex-col p-4 space-y-4">
|
||||||
|
<textarea
|
||||||
|
placeholder="Describe an app and let Gemini do the rest"
|
||||||
|
className="w-full bg-transparent resize-none focus:outline-none text-lg text-gray-700 min-h-[80px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button className="p-2 hover:bg-gray-100 rounded-full text-gray-400 transition-colors">
|
||||||
|
<Mic size={20} />
|
||||||
|
</button>
|
||||||
|
<button className="p-2 hover:bg-gray-100 rounded-full text-gray-400 transition-colors">
|
||||||
|
<Plus size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="flex items-center gap-2 px-5 py-2.5 bg-gray-50 hover:bg-gray-100 text-gray-700 rounded-full border border-gray-200 transition-all font-medium text-sm group/btn shadow-sm">
|
||||||
|
<Sparkles size={16} className="text-blue-500" />
|
||||||
|
I'm feeling lucky
|
||||||
|
<ArrowRight size={14} className="opacity-0 -translate-x-2 group-hover/btn:opacity-100 group-hover/btn:translate-x-0 transition-all" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Suggestion Chips */}
|
||||||
|
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||||
|
{suggestions.map((suggestion, idx) => (
|
||||||
|
<motion.button
|
||||||
|
key={suggestion.label}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 + idx * 0.05 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2.5 rounded-2xl border border-gray-100 bg-white hover:bg-gray-50 shadow-sm transition-all text-sm font-medium group hover:border-gray-200 text-gray-700`}
|
||||||
|
>
|
||||||
|
<suggestion.icon className={`w-4 h-4 ${suggestion.color}`} />
|
||||||
|
{suggestion.label}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
<motion.button
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
className="flex items-center justify-center p-2 rounded-full border border-gray-100 bg-white hover:bg-gray-50 text-gray-400 shadow-sm transition-all"
|
||||||
|
>
|
||||||
|
<ArrowRight size={16} />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer info/links */}
|
||||||
|
<footer className="w-full max-w-4xl mx-auto px-4 py-12 mt-auto">
|
||||||
|
<div className="rounded-3xl border border-gray-100 bg-gray-50/50 p-8 flex items-center justify-between group hover:bg-gray-50 transition-colors">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-display font-medium text-gray-800 mb-1">
|
||||||
|
Discover and remix app ideas
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-500 text-sm italic">
|
||||||
|
(Gallery view pending implementation)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button className="flex items-center gap-2 px-6 py-2.5 bg-white border border-gray-200 rounded-xl hover:shadow-md transition-all font-medium text-gray-700">
|
||||||
|
Browse the app gallery
|
||||||
|
<ArrowRight size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{/* Floating Right Bar (from target image) */}
|
||||||
|
<div className="absolute right-6 top-1/2 -translate-y-1/2 flex flex-col gap-4">
|
||||||
|
<button className="p-2.5 bg-white shadow-lg border border-gray-100 rounded-xl hover:scale-110 transition-all text-gray-400">
|
||||||
|
<LayoutGrid size={22} />
|
||||||
|
</button>
|
||||||
|
<button className="p-2.5 bg-white shadow-lg border border-gray-100 rounded-xl hover:scale-110 transition-all text-gray-400">
|
||||||
|
<Languages size={22} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display: "Outfit", "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-white text-gray-900 antialiased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.gemini-shadow {
|
||||||
|
box-shadow: 0 0 20px rgba(66, 133, 244, 0.08),
|
||||||
|
0 0 40px rgba(219, 68, 55, 0.04),
|
||||||
|
0 0 60px rgba(244, 180, 0, 0.04),
|
||||||
|
0 0 80px rgba(15, 157, 88, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gemini-gradient-border {
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
border-radius: 1.5rem; /* rounded-3xl */
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gemini-gradient-border::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 150%;
|
||||||
|
height: 150%;
|
||||||
|
top: -25%;
|
||||||
|
left: -25%;
|
||||||
|
background: conic-gradient(
|
||||||
|
from 0deg,
|
||||||
|
transparent 0deg,
|
||||||
|
transparent 90deg,
|
||||||
|
#4285F4 120deg,
|
||||||
|
#DB4437 150deg,
|
||||||
|
#F4B400 180deg,
|
||||||
|
#0F9D58 210deg,
|
||||||
|
transparent 240deg
|
||||||
|
);
|
||||||
|
z-index: -2;
|
||||||
|
animation: rotate 3s linear infinite;
|
||||||
|
filter: blur(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gemini-gradient-border::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 1.5px; /* The "thickness" of the glow border */
|
||||||
|
background: white;
|
||||||
|
border-radius: calc(1.5rem - 1px);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@400;500;600&display=swap');
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {StrictMode} from 'react';
|
||||||
|
import {createRoot} from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import {defineConfig} from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig(() => {
|
||||||
|
return {
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '.'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||||
|
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||||
|
hmr: process.env.DISABLE_HMR !== 'true',
|
||||||
|
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||||
|
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface CollapseTransitionProps {
|
||||||
|
open: boolean;
|
||||||
|
className?: string;
|
||||||
|
innerClassName?: string;
|
||||||
|
duration?: number;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CollapseTransition({
|
||||||
|
open,
|
||||||
|
className,
|
||||||
|
innerClassName,
|
||||||
|
duration = 200,
|
||||||
|
children,
|
||||||
|
}: CollapseTransitionProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden={!open}
|
||||||
|
style={{ transitionDuration: `${duration}ms` }}
|
||||||
|
className={cn(
|
||||||
|
'grid overflow-hidden transition-[grid-template-rows,opacity] ease-out',
|
||||||
|
open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0 pointer-events-none',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn('min-h-0 overflow-hidden', innerClassName)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
CheckCircle2, ChevronRight, ChevronDown,
|
CheckCircle2, ChevronRight,
|
||||||
ExternalLink, ListTree, FileText,
|
ExternalLink, ListTree, FileText,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -9,6 +9,7 @@ import type { AIMaterial, CompletedIntervention } from '../../types/ai-writing';
|
|||||||
import { OutlineCard } from './OutlineCard';
|
import { OutlineCard } from './OutlineCard';
|
||||||
import { ReviewCard } from './ReviewCard';
|
import { ReviewCard } from './ReviewCard';
|
||||||
import { ScoreBar } from './ScoreBar';
|
import { ScoreBar } from './ScoreBar';
|
||||||
|
import { CollapseTransition } from './CollapseTransition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
record: CompletedIntervention;
|
record: CompletedIntervention;
|
||||||
@@ -57,19 +58,22 @@ function MaterialCompleted({ record, expanded, onToggle }: { record: CompletedIn
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{count > 0 && (
|
{count > 0 && (
|
||||||
expanded
|
<ChevronRight
|
||||||
? <ChevronDown className="size-3 text-muted-foreground" />
|
className={cn(
|
||||||
: <ChevronRight className="size-3 text-muted-foreground" />
|
'size-3 text-muted-foreground transition-transform duration-200',
|
||||||
|
expanded && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded && record.materials && record.materials.length > 0 && (
|
<CollapseTransition open={expanded && !!record.materials && record.materials.length > 0}>
|
||||||
<div className="px-2.5 pb-2.5 pt-1 space-y-1.5 border-t border-green-200/40 dark:border-green-900/20">
|
<div className="px-2.5 pb-2.5 pt-1.5 space-y-1.5 border-t border-green-200/40 dark:border-green-900/20">
|
||||||
{record.materials.map((m) => (
|
{record.materials?.map((m) => (
|
||||||
<CompletedMaterialItem key={m.id} material={m} />
|
<CompletedMaterialItem key={m.id} material={m} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,9 +106,12 @@ function CompletedMaterialItem({ material }: { material: AIMaterial }) {
|
|||||||
<div className="flex items-start gap-1.5">
|
<div className="flex items-start gap-1.5">
|
||||||
<span className="flex-1 font-medium leading-snug line-clamp-2">{material.title}</span>
|
<span className="flex-1 font-medium leading-snug line-clamp-2">{material.title}</span>
|
||||||
{hasDetail && (
|
{hasDetail && (
|
||||||
open
|
<ChevronRight
|
||||||
? <ChevronDown className="mt-0.5 size-3 shrink-0 text-muted-foreground" />
|
className={cn(
|
||||||
: <ChevronRight className="mt-0.5 size-3 shrink-0 text-muted-foreground" />
|
'mt-0.5 size-3 shrink-0 text-muted-foreground transition-transform duration-200',
|
||||||
|
open && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
@@ -120,7 +127,7 @@ function CompletedMaterialItem({ material }: { material: AIMaterial }) {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && hasDetail && (
|
<CollapseTransition open={open && hasDetail}>
|
||||||
<div className="px-2.5 pb-2 pt-1.5 space-y-1.5 border-t border-border/40">
|
<div className="px-2.5 pb-2 pt-1.5 space-y-1.5 border-t border-border/40">
|
||||||
{material.content && (
|
{material.content && (
|
||||||
<p className="text-muted-foreground leading-relaxed line-clamp-6">
|
<p className="text-muted-foreground leading-relaxed line-clamp-6">
|
||||||
@@ -139,7 +146,7 @@ function CompletedMaterialItem({ material }: { material: AIMaterial }) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -168,17 +175,20 @@ function OutlineCompleted({ record, expanded, onToggle }: { record: CompletedInt
|
|||||||
<span className="ml-1 text-muted-foreground font-normal">共 {sectionCount} 章节</span>
|
<span className="ml-1 text-muted-foreground font-normal">共 {sectionCount} 章节</span>
|
||||||
</span>
|
</span>
|
||||||
{record.outline && (
|
{record.outline && (
|
||||||
expanded
|
<ChevronRight
|
||||||
? <ChevronDown className="size-3 text-muted-foreground" />
|
className={cn(
|
||||||
: <ChevronRight className="size-3 text-muted-foreground" />
|
'size-3 text-muted-foreground transition-transform duration-200',
|
||||||
|
expanded && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded && record.outline && (
|
<CollapseTransition open={expanded && !!record.outline}>
|
||||||
<div className="px-2.5 pb-2.5 pt-1 border-t border-green-200/40 dark:border-green-900/20">
|
<div className="px-2.5 pb-2.5 pt-1 border-t border-green-200/40 dark:border-green-900/20">
|
||||||
<OutlineCard outline={record.outline} readOnly />
|
{record.outline && <OutlineCard outline={record.outline} readOnly />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -217,17 +227,20 @@ function DraftCompleted({ record, expanded, onToggle }: { record: CompletedInter
|
|||||||
{wordCount > 0 && <span className="ml-1">({wordCount} 字)</span>}
|
{wordCount > 0 && <span className="ml-1">({wordCount} 字)</span>}
|
||||||
</span>
|
</span>
|
||||||
{hasReview && (
|
{hasReview && (
|
||||||
expanded
|
<ChevronRight
|
||||||
? <ChevronDown className="size-3 text-muted-foreground" />
|
className={cn(
|
||||||
: <ChevronRight className="size-3 text-muted-foreground" />
|
'size-3 text-muted-foreground transition-transform duration-200',
|
||||||
|
expanded && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded && record.reviewResult && (
|
<CollapseTransition open={expanded && !!record.reviewResult}>
|
||||||
<div className="px-2.5 pb-2.5 pt-1 border-t border-green-200/40 dark:border-green-900/20">
|
<div className="px-2.5 pb-2.5 pt-1 border-t border-green-200/40 dark:border-green-900/20">
|
||||||
<ReviewCard review={record.reviewResult} />
|
{record.reviewResult && <ReviewCard review={record.reviewResult} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -259,19 +272,26 @@ function ReviewCompleted({ record, expanded, onToggle }: { record: CompletedInte
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{hasReview && (
|
{hasReview && (
|
||||||
expanded
|
<ChevronRight
|
||||||
? <ChevronDown className="size-3 text-muted-foreground" />
|
className={cn(
|
||||||
: <ChevronRight className="size-3 text-muted-foreground" />
|
'size-3 text-muted-foreground transition-transform duration-200',
|
||||||
|
expanded && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded && record.reviewResult && (
|
<CollapseTransition open={expanded && !!record.reviewResult}>
|
||||||
<div className="px-2.5 pb-2.5 pt-1 space-y-1.5 border-t border-green-200/40 dark:border-green-900/20">
|
<div className="px-2.5 pb-2.5 pt-1 space-y-1.5 border-t border-green-200/40 dark:border-green-900/20">
|
||||||
<ScoreBar label="事实准确性" score={record.reviewResult.factScore} />
|
{record.reviewResult && (
|
||||||
<ScoreBar label="逻辑结构" score={record.reviewResult.logicScore} />
|
<>
|
||||||
<ScoreBar label="语言文字" score={record.reviewResult.languageScore} />
|
<ScoreBar label="事实准确性" score={record.reviewResult.factScore} />
|
||||||
|
<ScoreBar label="逻辑结构" score={record.reviewResult.logicScore} />
|
||||||
|
<ScoreBar label="语言文字" score={record.reviewResult.languageScore} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Brain, ChevronDown, ChevronRight } from 'lucide-react';
|
import { Brain, ChevronRight } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { StreamingDraftSection } from '../../types/ai-writing';
|
import type { StreamingDraftSection } from '../../types/ai-writing';
|
||||||
|
import { CollapseTransition } from './CollapseTransition';
|
||||||
|
|
||||||
interface DraftStreamingViewProps {
|
interface DraftStreamingViewProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -40,10 +41,10 @@ function SectionThinking({ thinkingText, isDone }: { thinkingText: string; isDon
|
|||||||
<Brain className="size-3 text-amber-500 shrink-0" />
|
<Brain className="size-3 text-amber-500 shrink-0" />
|
||||||
<span className="text-[11px] font-medium text-amber-600 dark:text-amber-400">思考过程</span>
|
<span className="text-[11px] font-medium text-amber-600 dark:text-amber-400">思考过程</span>
|
||||||
<span className="ml-auto text-amber-400">
|
<span className="ml-auto text-amber-400">
|
||||||
{open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
<ChevronRight className={cn('size-3 transition-transform duration-200', open && 'rotate-90')} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
<CollapseTransition open={open}>
|
||||||
<pre
|
<pre
|
||||||
ref={preRef}
|
ref={preRef}
|
||||||
className="text-[10px] font-mono leading-relaxed px-2.5 py-2 text-amber-700/70 dark:text-amber-300/60 whitespace-pre-wrap max-h-32 overflow-y-auto italic"
|
className="text-[10px] font-mono leading-relaxed px-2.5 py-2 text-amber-700/70 dark:text-amber-300/60 whitespace-pre-wrap max-h-32 overflow-y-auto italic"
|
||||||
@@ -53,7 +54,7 @@ function SectionThinking({ thinkingText, isDone }: { thinkingText: string; isDon
|
|||||||
<span className="inline-block w-0.5 h-3 bg-amber-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
|
<span className="inline-block w-0.5 h-3 bg-amber-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
|
||||||
)}
|
)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ChevronRight, ChevronDown, ExternalLink } from 'lucide-react';
|
import { ChevronRight, ExternalLink } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { buildIntranetSourceUrl } from '@/core/config';
|
import { buildIntranetSourceUrl } from '@/core/config';
|
||||||
import type { AIMaterial } from '../../types/ai-writing';
|
import type { AIMaterial } from '../../types/ai-writing';
|
||||||
|
import { CollapseTransition } from './CollapseTransition';
|
||||||
|
|
||||||
interface MaterialItemProps {
|
interface MaterialItemProps {
|
||||||
material: AIMaterial;
|
material: AIMaterial;
|
||||||
@@ -52,9 +53,12 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
|
|||||||
<span className={cn('flex-1 font-medium leading-snug line-clamp-2', !checked && 'text-muted-foreground')}>
|
<span className={cn('flex-1 font-medium leading-snug line-clamp-2', !checked && 'text-muted-foreground')}>
|
||||||
{material.title}
|
{material.title}
|
||||||
</span>
|
</span>
|
||||||
{expanded
|
<ChevronRight
|
||||||
? <ChevronDown className="mt-0.5 size-3 shrink-0 text-muted-foreground" />
|
className={cn(
|
||||||
: <ChevronRight className="mt-0.5 size-3 shrink-0 text-muted-foreground" />}
|
'mt-0.5 size-3 shrink-0 text-muted-foreground transition-transform duration-200',
|
||||||
|
expanded && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 展开内容 */}
|
{/* 展开内容 */}
|
||||||
{expanded && (
|
<CollapseTransition open={expanded}>
|
||||||
<div className="px-2.5 pb-2.5 pl-8 space-y-1.5 border-t">
|
<div className="px-2.5 pb-2.5 pl-8 space-y-1.5 border-t">
|
||||||
<p className="text-muted-foreground leading-relaxed pt-2 line-clamp-6">
|
<p className="text-muted-foreground leading-relaxed pt-2 line-clamp-6">
|
||||||
{material.content.slice(0, 300)}
|
{material.content.slice(0, 300)}
|
||||||
@@ -102,7 +106,7 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ChevronDown, ChevronRight, FileText } from 'lucide-react';
|
import { ChevronRight, FileText } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import type { AIMaterial } from '../../types/ai-writing';
|
import type { AIMaterial } from '../../types/ai-writing';
|
||||||
import { MaterialItem } from './MaterialItem';
|
import { MaterialItem } from './MaterialItem';
|
||||||
|
import { CollapseTransition } from './CollapseTransition';
|
||||||
|
|
||||||
interface MaterialsCardProps {
|
interface MaterialsCardProps {
|
||||||
materials: AIMaterial[];
|
materials: AIMaterial[];
|
||||||
@@ -27,14 +29,17 @@ export function MaterialsCard({ materials, selectedIds, onToggle }: MaterialsCar
|
|||||||
(共 {materials.length} 条,已选 {selectedIds.size} 条)
|
(共 {materials.length} 条,已选 {selectedIds.size} 条)
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{expanded
|
<ChevronRight
|
||||||
? <ChevronDown className="size-3 text-muted-foreground" />
|
className={cn(
|
||||||
: <ChevronRight className="size-3 text-muted-foreground" />}
|
'size-3 text-muted-foreground transition-transform duration-200',
|
||||||
|
expanded && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 列表 */}
|
{/* 列表 */}
|
||||||
{expanded && (
|
<CollapseTransition open={expanded}>
|
||||||
<div className="px-2.5 pb-2.5 space-y-1.5 border-t">
|
<div className="px-2.5 pb-2.5 pt-1.5 space-y-1.5 border-t">
|
||||||
{materials.length === 0 ? (
|
{materials.length === 0 ? (
|
||||||
<p className="text-xs text-muted-foreground py-2 text-center">暂无素材</p>
|
<p className="text-xs text-muted-foreground py-2 text-center">暂无素材</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -48,7 +53,7 @@ export function MaterialsCard({ materials, selectedIds, onToggle }: MaterialsCar
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { ListTree, Plus } from 'lucide-react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type { AIOutline, OutlineSection as OutlineSectionType } from '../../types/ai-writing';
|
import type { AIOutline, OutlineSection as OutlineSectionType } from '../../types/ai-writing';
|
||||||
import { OutlineSection } from './OutlineSection';
|
import { OutlineSection } from './OutlineSection';
|
||||||
|
import { CollapseTransition } from './CollapseTransition';
|
||||||
|
|
||||||
interface OutlineCardProps {
|
interface OutlineCardProps {
|
||||||
outline: AIOutline;
|
outline: AIOutline;
|
||||||
@@ -69,7 +70,7 @@ export function OutlineCard({ outline, onChange, readOnly = false }: OutlineCard
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!collapsed && (
|
<CollapseTransition open={!collapsed}>
|
||||||
<div className="px-3 py-2.5 space-y-2">
|
<div className="px-3 py-2.5 space-y-2">
|
||||||
{/* 文章标题 */}
|
{/* 文章标题 */}
|
||||||
<div className="flex items-center gap-1.5 text-xs">
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
@@ -137,7 +138,7 @@ export function OutlineCard({ outline, onChange, readOnly = false }: OutlineCard
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ChevronRight, ChevronDown, Plus, X, GripVertical } from 'lucide-react';
|
import { ChevronRight, Plus, X, GripVertical } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { OutlineSection as OutlineSectionType } from '../../types/ai-writing';
|
import type { OutlineSection as OutlineSectionType } from '../../types/ai-writing';
|
||||||
|
import { CollapseTransition } from './CollapseTransition';
|
||||||
|
|
||||||
interface OutlineSectionProps {
|
interface OutlineSectionProps {
|
||||||
section: OutlineSectionType;
|
section: OutlineSectionType;
|
||||||
@@ -72,9 +73,12 @@ export function OutlineSection({
|
|||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
>
|
>
|
||||||
{expanded
|
<ChevronRight
|
||||||
? <ChevronDown className="size-3.5 text-muted-foreground" />
|
className={cn(
|
||||||
: <ChevronRight className="size-3.5 text-muted-foreground" />}
|
'size-3.5 text-muted-foreground transition-transform duration-200',
|
||||||
|
expanded && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="text-muted-foreground/60 shrink-0 w-4 text-center">{index + 1}.</span>
|
<span className="text-muted-foreground/60 shrink-0 w-4 text-center">{index + 1}.</span>
|
||||||
@@ -109,7 +113,7 @@ export function OutlineSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 要点列表 */}
|
{/* 要点列表 */}
|
||||||
{expanded && (
|
<CollapseTransition open={expanded}>
|
||||||
<div className="px-8 pb-2.5 space-y-1 border-t">
|
<div className="px-8 pb-2.5 space-y-1 border-t">
|
||||||
{section.keyPoints.map((kp, i) => (
|
{section.keyPoints.map((kp, i) => (
|
||||||
<div key={i} className="flex items-center gap-1.5 pt-1.5">
|
<div key={i} className="flex items-center gap-1.5 pt-1.5">
|
||||||
@@ -142,7 +146,7 @@ export function OutlineSection({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CollapseTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user