fix: polish layout and app branding
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 800 B After Width: | Height: | Size: 15 KiB |
@@ -54,7 +54,9 @@ pub async fn generate_image(
|
|||||||
let model = input
|
let model = input
|
||||||
.model
|
.model
|
||||||
.clone()
|
.clone()
|
||||||
|
.filter(|model| !model.trim().is_empty())
|
||||||
.or_else(|| provider.image_model.clone())
|
.or_else(|| provider.image_model.clone())
|
||||||
|
.filter(|model| !model.trim().is_empty())
|
||||||
.ok_or_else(|| AppError::Provider("image model is empty".to_string()))?;
|
.ok_or_else(|| AppError::Provider("image model is empty".to_string()))?;
|
||||||
|
|
||||||
let task = repository::create_generation_task(
|
let task = repository::create_generation_task(
|
||||||
|
|||||||
@@ -13,23 +13,23 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Image Draw AI",
|
"title": "Image Draw AI",
|
||||||
"width": 1100,
|
"width": 1280,
|
||||||
"height": 760,
|
"height": 1160,
|
||||||
"minWidth": 900,
|
"minWidth": 1180,
|
||||||
"minHeight": 620
|
"minHeight": 1100
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null,
|
"csp": null,
|
||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"scope": ["$APPDATA/**"]
|
"scope": ["$APPDATA/**", "$HOME/**", "$PICTURE/**", "$DOWNLOAD/**", "$DESKTOP/**", "$DOCUMENT/**"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"icon": []
|
"icon": ["icons/icon.png"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/assets/logo.svg
Normal file
1
src/assets/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.5 KiB |
171
src/main.tsx
171
src/main.tsx
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { convertFileSrc, invoke } from '@tauri-apps/api/core';
|
import { convertFileSrc, invoke } from '@tauri-apps/api/core';
|
||||||
|
import appLogo from './assets/logo.svg';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
type ProviderConfig = {
|
type ProviderConfig = {
|
||||||
@@ -20,8 +21,8 @@ type ProviderForm = {
|
|||||||
kind: string;
|
kind: string;
|
||||||
base_url: string;
|
base_url: string;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
text_model: string;
|
text_model?: string | null;
|
||||||
image_model: string;
|
image_model?: string | null;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,8 +61,8 @@ const defaultProviderForm: ProviderForm = {
|
|||||||
kind: 'openai-compatible',
|
kind: 'openai-compatible',
|
||||||
base_url: 'https://api.openai.com/v1',
|
base_url: 'https://api.openai.com/v1',
|
||||||
api_key: '',
|
api_key: '',
|
||||||
text_model: 'gpt-5',
|
text_model: null,
|
||||||
image_model: 'gpt-image-2',
|
image_model: null,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,10 +74,15 @@ const initialGenerationSteps: GenerationStep[] = [
|
|||||||
{ label: '更新结果列表', status: 'pending' },
|
{ label: '更新结果列表', status: 'pending' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const imageModelOptions = ['gpt-image-1', 'gpt-image-1.5', 'gpt-image-2'];
|
||||||
|
const imageSizeOptions = ['1024x1024', '1024x1536', '1536x1024'];
|
||||||
|
const imageQualityOptions = ['auto', 'high', 'medium', 'low'];
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [providers, setProviders] = useState<ProviderConfig[]>([]);
|
const [providers, setProviders] = useState<ProviderConfig[]>([]);
|
||||||
const [providerForm, setProviderForm] = useState<ProviderForm>(defaultProviderForm);
|
const [providerForm, setProviderForm] = useState<ProviderForm>(defaultProviderForm);
|
||||||
const [prompt, setPrompt] = useState('一只赛博朋克风格的橘猫坐在霓虹灯下');
|
const [prompt, setPrompt] = useState('一只赛博朋克风格的橘猫坐在霓虹灯下');
|
||||||
|
const [selectedImageModel, setSelectedImageModel] = useState('gpt-image-2');
|
||||||
const [imageSize, setImageSize] = useState('1024x1024');
|
const [imageSize, setImageSize] = useState('1024x1024');
|
||||||
const [imageQuality, setImageQuality] = useState('auto');
|
const [imageQuality, setImageQuality] = useState('auto');
|
||||||
const [status, setStatus] = useState('准备就绪');
|
const [status, setStatus] = useState('准备就绪');
|
||||||
@@ -119,8 +125,8 @@ function App() {
|
|||||||
kind: current.kind,
|
kind: current.kind,
|
||||||
base_url: current.base_url,
|
base_url: current.base_url,
|
||||||
api_key: current.api_key ?? form.api_key,
|
api_key: current.api_key ?? form.api_key,
|
||||||
text_model: current.text_model ?? defaultProviderForm.text_model,
|
text_model: current.text_model ?? null,
|
||||||
image_model: current.image_model ?? defaultProviderForm.image_model,
|
image_model: null,
|
||||||
enabled: current.enabled,
|
enabled: current.enabled,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -130,7 +136,7 @@ function App() {
|
|||||||
setIsBusy(true);
|
setIsBusy(true);
|
||||||
setStatus('正在保存配置...');
|
setStatus('正在保存配置...');
|
||||||
try {
|
try {
|
||||||
await invoke('upsert_provider', { input: providerForm });
|
await invoke('upsert_provider', { input: { ...providerForm, image_model: null } });
|
||||||
await refreshProviders();
|
await refreshProviders();
|
||||||
setStatus('配置已保存');
|
setStatus('配置已保存');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -165,8 +171,8 @@ function App() {
|
|||||||
kind: provider.kind,
|
kind: provider.kind,
|
||||||
base_url: provider.base_url,
|
base_url: provider.base_url,
|
||||||
api_key: provider.api_key ?? form.api_key,
|
api_key: provider.api_key ?? form.api_key,
|
||||||
text_model: provider.text_model ?? defaultProviderForm.text_model,
|
text_model: provider.text_model ?? null,
|
||||||
image_model: provider.image_model ?? defaultProviderForm.image_model,
|
image_model: null,
|
||||||
enabled: provider.enabled,
|
enabled: provider.enabled,
|
||||||
}));
|
}));
|
||||||
setStatus('已切换模型配置');
|
setStatus('已切换模型配置');
|
||||||
@@ -178,7 +184,7 @@ function App() {
|
|||||||
setStatus('正在生成图片...');
|
setStatus('正在生成图片...');
|
||||||
try {
|
try {
|
||||||
startStep(0);
|
startStep(0);
|
||||||
await invoke('upsert_provider', { input: providerForm });
|
await invoke('upsert_provider', { input: { ...providerForm, image_model: null } });
|
||||||
startStep(1);
|
startStep(1);
|
||||||
await new Promise((resolve) => window.setTimeout(resolve, 120));
|
await new Promise((resolve) => window.setTimeout(resolve, 120));
|
||||||
startStep(2);
|
startStep(2);
|
||||||
@@ -186,7 +192,7 @@ function App() {
|
|||||||
input: {
|
input: {
|
||||||
provider_id: providerForm.id,
|
provider_id: providerForm.id,
|
||||||
prompt,
|
prompt,
|
||||||
model: providerForm.image_model,
|
model: selectedImageModel,
|
||||||
size: imageSize,
|
size: imageSize,
|
||||||
quality: imageQuality,
|
quality: imageQuality,
|
||||||
image_paths: materialPaths,
|
image_paths: materialPaths,
|
||||||
@@ -260,7 +266,7 @@ function App() {
|
|||||||
<main className="app-shell">
|
<main className="app-shell">
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="brand">
|
<div className="brand">
|
||||||
<div className="brand-mark">绘</div>
|
<img className="brand-mark" src={appLogo} alt="Image Draw AI" />
|
||||||
<div>
|
<div>
|
||||||
<h1>Image Draw AI</h1>
|
<h1>Image Draw AI</h1>
|
||||||
<p>图片默认保存到应用数据文件夹</p>
|
<p>图片默认保存到应用数据文件夹</p>
|
||||||
@@ -269,7 +275,7 @@ function App() {
|
|||||||
<div className="topbar-actions">
|
<div className="topbar-actions">
|
||||||
<div className="current-provider">
|
<div className="current-provider">
|
||||||
<span>当前模型</span>
|
<span>当前模型</span>
|
||||||
<strong>{providerForm.image_model}</strong>
|
<strong>{selectedImageModel}</strong>
|
||||||
</div>
|
</div>
|
||||||
<button className="ghost" onClick={() => setIsSettingsOpen(true)}>设置</button>
|
<button className="ghost" onClick={() => setIsSettingsOpen(true)}>设置</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,25 +294,30 @@ function App() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="material-panel">
|
<div className="material-panel">
|
||||||
<div className="material-toolbar">
|
<div className="material-header">
|
||||||
<button onClick={pickMaterialImages} disabled={isBusy}>导入素材</button>
|
<div>
|
||||||
{materialPaths.length > 0 && (
|
<strong>参考图</strong>
|
||||||
<button className="ghost" onClick={() => setMaterialPaths([])} disabled={isBusy}>清空</button>
|
<span>{materialPaths.length > 0 ? `${materialPaths.length} 张,图像编辑模式` : '可选,支持多张'}</span>
|
||||||
)}
|
|
||||||
<span>{materialPaths.length > 0 ? `${materialPaths.length} 张素材` : '未导入素材'}</span>
|
|
||||||
</div>
|
|
||||||
<p className="drop-hint">支持 PNG / JPG / WEBP,多张素材会使用图像编辑模式</p>
|
|
||||||
|
|
||||||
{materialPaths.length > 0 && (
|
|
||||||
<div className="material-grid">
|
|
||||||
{materialPaths.map((path) => (
|
|
||||||
<article className="material-card" key={path}>
|
|
||||||
<img src={convertFileSrc(path)} alt="素材图片" />
|
|
||||||
<button onClick={() => removeMaterialImage(path)} disabled={isBusy}>移除</button>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{materialPaths.length > 0 && (
|
||||||
|
<button className="ghost mini" onClick={() => setMaterialPaths([])} disabled={isBusy}>清空</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="reference-strip">
|
||||||
|
<button className="add-reference-card" onClick={pickMaterialImages} disabled={isBusy}>
|
||||||
|
<span>+</span>
|
||||||
|
<strong>添加参考图</strong>
|
||||||
|
<small>PNG/JPG/WEBP</small>
|
||||||
|
</button>
|
||||||
|
{materialPaths.map((path, index) => (
|
||||||
|
<article className="reference-card" key={path}>
|
||||||
|
<img src={convertFileSrc(path)} alt="素材图片" />
|
||||||
|
<span>{index + 1}</span>
|
||||||
|
<button onClick={() => removeMaterialImage(path)} disabled={isBusy}>×</button>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="params-card">
|
<div className="params-card">
|
||||||
@@ -314,29 +325,31 @@ function App() {
|
|||||||
<span>生成参数</span>
|
<span>生成参数</span>
|
||||||
<strong>基础</strong>
|
<strong>基础</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="segmented">
|
<label className="field compact-field">
|
||||||
{['1024x1024', '1024x1536', '1536x1024'].map((size) => (
|
<span>图像模型</span>
|
||||||
<button
|
<select value={selectedImageModel} onChange={(event) => setSelectedImageModel(event.target.value)} disabled={isBusy}>
|
||||||
className={imageSize === size ? 'active' : ''}
|
{imageModelOptions.map((model) => (
|
||||||
key={size}
|
<option key={model} value={model}>{model}</option>
|
||||||
onClick={() => setImageSize(size)}
|
))}
|
||||||
disabled={isBusy}
|
</select>
|
||||||
>
|
</label>
|
||||||
{size}
|
<div className="grid two">
|
||||||
</button>
|
<label className="field compact-field">
|
||||||
))}
|
<span>分辨率</span>
|
||||||
</div>
|
<select value={imageSize} onChange={(event) => setImageSize(event.target.value)} disabled={isBusy}>
|
||||||
<div className="segmented compact">
|
{imageSizeOptions.map((size) => (
|
||||||
{['auto', 'high', 'medium', 'low'].map((quality) => (
|
<option key={size} value={size}>{size}</option>
|
||||||
<button
|
))}
|
||||||
className={imageQuality === quality ? 'active' : ''}
|
</select>
|
||||||
key={quality}
|
</label>
|
||||||
onClick={() => setImageQuality(quality)}
|
<label className="field compact-field">
|
||||||
disabled={isBusy}
|
<span>质量</span>
|
||||||
>
|
<select value={imageQuality} onChange={(event) => setImageQuality(event.target.value)} disabled={isBusy}>
|
||||||
{quality}
|
{imageQualityOptions.map((quality) => (
|
||||||
</button>
|
<option key={quality} value={quality}>{quality}</option>
|
||||||
))}
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -344,22 +357,7 @@ function App() {
|
|||||||
{isBusy ? '正在生成...' : '开始生成'}
|
{isBusy ? '正在生成...' : '开始生成'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={`progress-card ${isBusy ? 'is-loading' : ''}`}>
|
{status !== '准备就绪' && <p className="status">{status}</p>}
|
||||||
<div className="spinner" aria-hidden="true" />
|
|
||||||
<div className="progress-content">
|
|
||||||
<strong>{isBusy ? '生成中' : '生成流程'}</strong>
|
|
||||||
<ol className="step-list">
|
|
||||||
{generationSteps.map((step) => (
|
|
||||||
<li className={`step ${step.status}`} key={step.label}>
|
|
||||||
<span />
|
|
||||||
{step.label}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="status">{status}</p>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="result-card">
|
<section className="result-card">
|
||||||
@@ -393,6 +391,21 @@ function App() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className={`progress-card result-progress ${isBusy ? 'is-loading' : ''}`}>
|
||||||
|
<div className="spinner" aria-hidden="true" />
|
||||||
|
<div className="progress-content">
|
||||||
|
<strong>{isBusy ? '生成中' : '生成流程'}</strong>
|
||||||
|
<ol className="step-list">
|
||||||
|
{generationSteps.map((step) => (
|
||||||
|
<li className={`step ${step.status}`} key={step.label}>
|
||||||
|
<span />
|
||||||
|
{step.label}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -452,23 +465,6 @@ function App() {
|
|||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="settings-group">
|
|
||||||
<div className="section-heading">
|
|
||||||
<span>模型</span>
|
|
||||||
<strong>默认模型</strong>
|
|
||||||
</div>
|
|
||||||
<div className="grid two">
|
|
||||||
<label className="field">
|
|
||||||
<span>文本模型</span>
|
|
||||||
<input value={providerForm.text_model} onChange={(event) => updateProviderForm('text_model', event.target.value)} />
|
|
||||||
</label>
|
|
||||||
<label className="field">
|
|
||||||
<span>图像模型</span>
|
|
||||||
<input value={providerForm.image_model} onChange={(event) => updateProviderForm('image_model', event.target.value)} />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="drawer-actions">
|
<div className="drawer-actions">
|
||||||
<button onClick={saveProvider} disabled={isBusy}>保存配置</button>
|
<button onClick={saveProvider} disabled={isBusy}>保存配置</button>
|
||||||
<button className="ghost" onClick={refreshProviders} disabled={isBusy}>刷新</button>
|
<button className="ghost" onClick={refreshProviders} disabled={isBusy}>刷新</button>
|
||||||
@@ -489,7 +485,6 @@ function App() {
|
|||||||
<button className="link-button" onClick={() => loadProvider(provider)} disabled={isBusy}>
|
<button className="link-button" onClick={() => loadProvider(provider)} disabled={isBusy}>
|
||||||
<strong>{provider.name}</strong>
|
<strong>{provider.name}</strong>
|
||||||
<span>{provider.base_url}</span>
|
<span>{provider.base_url}</span>
|
||||||
<span>{provider.image_model}</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button className="danger" onClick={() => deleteProvider(provider.id)} disabled={isBusy}>删除</button>
|
<button className="danger" onClick={() => deleteProvider(provider.id)} disabled={isBusy}>删除</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
329
src/styles.css
329
src/styles.css
@@ -1,6 +1,9 @@
|
|||||||
:root {
|
:root {
|
||||||
color: #172033;
|
color: #172033;
|
||||||
background: #eef3fb;
|
background:
|
||||||
|
radial-gradient(circle at 8% 10%, rgba(255, 221, 77, 0.22), transparent 28%),
|
||||||
|
radial-gradient(circle at 92% 8%, rgba(96, 239, 154, 0.18), transparent 24%),
|
||||||
|
linear-gradient(135deg, #f7f9ff 0%, #eef4fb 46%, #f8fbf6 100%);
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,9 +13,10 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, textarea, input {
|
button, textarea, input, select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,21 +58,28 @@ button.mini {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
min-height: 100vh;
|
display: grid;
|
||||||
padding: 22px;
|
gap: 18px;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
height: 100vh;
|
||||||
|
min-width: 1120px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: rgba(255, 255, 255, 0.82);
|
backdrop-filter: blur(18px);
|
||||||
border: 1px solid rgba(203, 213, 225, 0.8);
|
background: rgba(255, 255, 255, 0.76);
|
||||||
border-radius: 24px;
|
border: 1px solid rgba(255, 255, 255, 0.86);
|
||||||
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
|
border-radius: 28px;
|
||||||
|
box-shadow: 0 20px 60px rgba(30, 41, 59, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 0 auto 18px;
|
margin: 0 auto;
|
||||||
max-width: 1440px;
|
max-width: 1440px;
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
@@ -78,15 +89,12 @@ button.mini {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark {
|
.brand-mark {
|
||||||
align-items: center;
|
background: linear-gradient(180deg, #ffffff, #f8fafc);
|
||||||
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
color: #ffffff;
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12), inset 0 0 0 1px rgba(15, 23, 42, 0.08);
|
||||||
display: grid;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 900;
|
|
||||||
height: 52px;
|
height: 52px;
|
||||||
place-items: center;
|
object-fit: contain;
|
||||||
|
padding: 6px;
|
||||||
width: 52px;
|
width: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +116,8 @@ button.mini {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.current-provider {
|
.current-provider {
|
||||||
background: #f8fafc;
|
background: rgba(248, 250, 252, 0.88);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid rgba(226, 232, 240, 0.96);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -129,29 +137,56 @@ button.mini {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
|
align-items: stretch;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
grid-template-columns: minmax(360px, 440px) minmax(0, 1fr);
|
grid-template-columns: minmax(360px, 440px) minmax(0, 1fr);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 1440px;
|
max-width: 1440px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-card, .result-card, .settings-drawer {
|
.compose-card, .result-card, .settings-drawer {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
backdrop-filter: blur(18px);
|
||||||
border: 1px solid rgba(203, 213, 225, 0.9);
|
background: rgba(255, 255, 255, 0.86);
|
||||||
border-radius: 28px;
|
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.1);
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 28px 80px rgba(30, 41, 59, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.74);
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-card {
|
.compose-card {
|
||||||
align-self: start;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-card > .section-heading,
|
||||||
|
.compose-card > .material-panel,
|
||||||
|
.compose-card > .params-card,
|
||||||
|
.compose-card > .generate-button,
|
||||||
|
.compose-card > .status {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 18px;
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
padding: 20px;
|
flex: 1 1 auto;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 260px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-card {
|
.result-card {
|
||||||
min-height: calc(100vh - 132px);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +195,7 @@ button.mini {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
min-height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading-actions {
|
.heading-actions {
|
||||||
@@ -180,25 +216,36 @@ button.mini {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea {
|
input, textarea, select {
|
||||||
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #dbe4f0;
|
border: 1px solid #dbe4f0;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
color: #172033;
|
color: #172033;
|
||||||
background: #f8fafc;
|
background: rgba(248, 250, 252, 0.88);
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 13px 14px;
|
padding: 13px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus, textarea:focus {
|
input:focus, textarea:focus, select:focus {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-color: #60a5fa;
|
border-color: #60a5fa;
|
||||||
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.18);
|
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
appearance: none;
|
||||||
|
background-image: linear-gradient(45deg, transparent 50%, #64748b 50%), linear-gradient(135deg, #64748b 50%, transparent 50%);
|
||||||
|
background-position: calc(100% - 18px) 52%, calc(100% - 12px) 52%;
|
||||||
|
background-size: 6px 6px, 6px 6px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.prompt-field textarea {
|
.prompt-field textarea {
|
||||||
min-height: 190px;
|
height: 100%;
|
||||||
resize: vertical;
|
min-height: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
small {
|
small {
|
||||||
@@ -216,76 +263,154 @@ small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.material-panel {
|
.material-panel {
|
||||||
background: #f8fafc;
|
background: rgba(248, 250, 252, 0.72);
|
||||||
border: 1px dashed #cbd5e1;
|
border: 1px solid rgba(226, 232, 240, 0.92);
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
padding: 14px;
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-rows: auto 120px;
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-toolbar {
|
.material-header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-toolbar span {
|
.material-header div {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-header strong {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-header span {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reference-strip {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 120px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding: 8px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-reference-card, .reference-card {
|
||||||
|
border-radius: 18px;
|
||||||
|
flex: 0 0 104px;
|
||||||
|
height: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-reference-card {
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(180deg, #ffffff, #f8fafc);
|
||||||
|
border: 1px dashed rgba(37, 99, 235, 0.42);
|
||||||
|
color: #2563eb;
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
justify-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-reference-card:hover:not(:disabled) {
|
||||||
|
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-reference-card > span {
|
||||||
|
align-items: center;
|
||||||
|
background: #eff6ff;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: grid;
|
||||||
|
font-size: 22px;
|
||||||
|
height: 34px;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-reference-card strong {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-hint {
|
.add-reference-card small {
|
||||||
color: #94a3b8;
|
font-size: 11px;
|
||||||
font-size: 12px;
|
|
||||||
margin: 10px 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-grid {
|
.reference-card {
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-card {
|
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 18px;
|
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.08);
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 8px;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-card img {
|
.reference-card img {
|
||||||
aspect-ratio: 1;
|
|
||||||
background: #e2e8f0;
|
background: #e2e8f0;
|
||||||
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-card button {
|
.reference-card button {
|
||||||
justify-self: center;
|
align-items: center;
|
||||||
padding: 6px 10px;
|
background: rgba(15, 23, 42, 0.72);
|
||||||
|
border-radius: 999px;
|
||||||
|
display: grid;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
position: absolute;
|
||||||
|
right: 7px;
|
||||||
|
top: 7px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reference-card > span {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
border-radius: 999px;
|
||||||
|
bottom: 7px;
|
||||||
|
color: #334155;
|
||||||
|
display: grid;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
height: 24px;
|
||||||
|
left: 7px;
|
||||||
|
place-items: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.generate-button {
|
.generate-button {
|
||||||
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
background: linear-gradient(135deg, #2563eb, #7c3aed 58%, #f97316);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 18px 36px rgba(37, 99, 235, 0.28);
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
padding: 15px 18px;
|
padding: 15px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-card {
|
.progress-card {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
background: #f8fafc;
|
background: rgba(248, 250, 252, 0.82);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid rgba(226, 232, 240, 0.92);
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-progress {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
border: 3px solid rgba(148, 163, 184, 0.32);
|
border: 3px solid rgba(148, 163, 184, 0.32);
|
||||||
border-top-color: #2563eb;
|
border-top-color: #2563eb;
|
||||||
@@ -374,13 +499,21 @@ small {
|
|||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px dashed #cbd5e1;
|
background: linear-gradient(180deg, rgba(248, 250, 252, 0.5), rgba(255, 255, 255, 0.76));
|
||||||
|
border: 1px dashed rgba(148, 163, 184, 0.68);
|
||||||
border-radius: 26px;
|
border-radius: 26px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
flex: 1;
|
||||||
min-height: 420px;
|
min-height: 420px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state > div {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state div {
|
.empty-state div {
|
||||||
@@ -390,17 +523,22 @@ small {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.image-grid {
|
.image-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-card {
|
.image-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid rgba(226, 232, 240, 0.9);
|
||||||
border-radius: 22px;
|
border-radius: 24px;
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
|
box-shadow: 0 18px 46px rgba(15, 23, 42, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +625,11 @@ small {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawer-actions button {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.saved-providers {
|
.saved-providers {
|
||||||
border-top: 1px solid #e2e8f0;
|
border-top: 1px solid #e2e8f0;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -536,28 +679,11 @@ small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.workspace {
|
.workspace { grid-template-columns: minmax(360px, 440px) minmax(0, 1fr); }
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-card {
|
|
||||||
min-height: 420px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.app-shell {
|
.grid.two, .provider-item { grid-template-columns: 1fr; }
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar, .topbar-actions {
|
|
||||||
align-items: stretch;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid.two, .provider-item {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.params-card {
|
.params-card {
|
||||||
@@ -565,37 +691,18 @@ small {
|
|||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
padding: 14px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented {
|
.compact-field {
|
||||||
background: #eef2f7;
|
|
||||||
border-radius: 16px;
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
padding: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented.compact {
|
.compact-field input,
|
||||||
grid-template-columns: repeat(4, 1fr);
|
.compact-field select {
|
||||||
}
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
.segmented button {
|
|
||||||
background: transparent;
|
|
||||||
color: #64748b;
|
|
||||||
padding: 9px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented button:hover:not(:disabled) {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented button.active {
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
|
||||||
color: #1d4ed8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview-button {
|
.image-preview-button {
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
Reference in New Issue
Block a user