Professionelles Redesign + Directus-Auth + Tag/Nacht-Modus
- Vollbild App-Shell mit Topbar, drei-Spalten-Workspace - Login-Seite mit Directus JWT-Authentifizierung (in-memory Token) - Tag/Nacht-Modus mit CSS Custom Properties (Systemfarbe als Default) - Directus 'pictures' Collection (status=new) als Bildquelle in DrawIt - Pfeil-Navigation durch Bilder mit Bildnummer-Anzeige - Neues Design-System: Indigo-Akzent, SVG-Icons, professionelle Typografie - ThemeProvider, AuthProvider, PrivateRoute, Topbar-Komponente Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,36 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import ObjectsList from '../components/ObjectsList'
|
||||
import DetailsPanel from '../components/DetailsPanel'
|
||||
import SentencesList from '../components/SentencesList'
|
||||
import Topbar from '../components/Topbar'
|
||||
import { getImages, getObjects, generateDetails, generateSentence, getSentences } from '../api'
|
||||
import type { ObjectMeta, Sentence } from '../types'
|
||||
|
||||
export default function GenerateIt() {
|
||||
const navigate = useNavigate()
|
||||
const ChevronLeftIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChevronRightIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SparkleIcon = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChatIcon = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function GenerateIt() {
|
||||
const [imageList, setImageList] = useState<string[]>([])
|
||||
const [currentIndex, setCurrentIndex] = useState(-1)
|
||||
const [objects, setObjects] = useState<ObjectMeta[]>([])
|
||||
@@ -52,8 +74,7 @@ export default function GenerateIt() {
|
||||
|
||||
const loadSentences = async (objId: string) => {
|
||||
try {
|
||||
const s = await getSentences(objId)
|
||||
setSentences(s)
|
||||
setSentences(await getSentences(objId))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
@@ -90,76 +111,103 @@ export default function GenerateIt() {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>GenerateIt</h1>
|
||||
|
||||
<div className="panel image-nav">
|
||||
<div className="image-nav-left">
|
||||
<button
|
||||
onClick={() => setCurrentIndex(i => i - 1)}
|
||||
disabled={currentIndex <= 0}
|
||||
>
|
||||
⬅️
|
||||
</button>
|
||||
<span>Bild: <code>{currentFilename || '–'}</code></span>
|
||||
<button
|
||||
onClick={() => setCurrentIndex(i => i + 1)}
|
||||
disabled={currentIndex >= imageList.length - 1}
|
||||
>
|
||||
➡️
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerateDetails}
|
||||
disabled={isGeneratingDetails || !selectedObj}
|
||||
>
|
||||
{isGeneratingDetails ? '⏳ KI-Details...' : '✨ KI-Details'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerateSentence}
|
||||
disabled={isGeneratingSentence || !selectedObj}
|
||||
>
|
||||
{isGeneratingSentence ? '⏳ KI-Sentence...' : '💬 KI-Sentence'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="page-switch">
|
||||
<select value="/generate" onChange={e => navigate(e.target.value)}>
|
||||
<option value="/draw">DrawIt</option>
|
||||
<option value="/generate">GenerateIt</option>
|
||||
</select>
|
||||
</div>
|
||||
const imageNav = (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div className="image-nav">
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => setCurrentIndex(i => i - 1)}
|
||||
disabled={currentIndex <= 0}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</button>
|
||||
<span className="image-counter">
|
||||
{imageList.length > 0 ? (
|
||||
<>
|
||||
<span className="image-counter-num">{currentIndex + 1}</span>
|
||||
<span className="image-counter-sep">/</span>
|
||||
<span className="image-counter-total">{imageList.length}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="image-counter-empty">Keine Bilder</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => setCurrentIndex(i => i + 1)}
|
||||
disabled={currentIndex >= imageList.length - 1}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="main-layout">
|
||||
<div className="objects-pane sidebar-section">
|
||||
<h2>Objekte zu diesem Bild</h2>
|
||||
<ObjectsList
|
||||
objects={objects}
|
||||
selectedObjectId={selectedObj?.id ?? null}
|
||||
onSelect={id => {
|
||||
const obj = objects.find(o => o.id === id)
|
||||
if (obj) {
|
||||
setSelectedObj(obj)
|
||||
loadSentences(obj.id)
|
||||
}
|
||||
}}
|
||||
onObjectsChange={setObjects}
|
||||
isGeneratePage={true}
|
||||
onShowDetails={obj => setSelectedObj(obj)}
|
||||
onLoadSentences={loadSentences}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: 1, height: 24, background: 'var(--border)' }} />
|
||||
|
||||
<div className="right-pane">
|
||||
<DetailsPanel obj={selectedObj} objects={objects} sentences={sentences} />
|
||||
</div>
|
||||
<button
|
||||
className="btn-ghost btn-sm"
|
||||
onClick={handleGenerateDetails}
|
||||
disabled={isGeneratingDetails || !selectedObj}
|
||||
>
|
||||
<SparkleIcon />
|
||||
{isGeneratingDetails ? 'Generiere…' : 'KI-Details'}
|
||||
</button>
|
||||
<button
|
||||
className="btn-ghost btn-sm"
|
||||
onClick={handleGenerateSentence}
|
||||
disabled={isGeneratingSentence || !selectedObj}
|
||||
>
|
||||
<ChatIcon />
|
||||
{isGeneratingSentence ? 'Generiere…' : 'KI-Sentence'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
<div className="sentences-pane">
|
||||
<div className="sidebar-section">
|
||||
<h2>Alle Sätze</h2>
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<Topbar page="generate" center={imageNav} />
|
||||
|
||||
<div className="workspace">
|
||||
{/* Left: Objects */}
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-panel" style={{ flex: 1 }}>
|
||||
<h3 className="sidebar-heading">Objekte</h3>
|
||||
<ObjectsList
|
||||
objects={objects}
|
||||
selectedObjectId={selectedObj?.id ?? null}
|
||||
onSelect={id => {
|
||||
const obj = objects.find(o => o.id === id)
|
||||
if (obj) {
|
||||
setSelectedObj(obj)
|
||||
loadSentences(obj.id)
|
||||
}
|
||||
}}
|
||||
onObjectsChange={setObjects}
|
||||
isGeneratePage={true}
|
||||
onShowDetails={obj => setSelectedObj(obj)}
|
||||
onLoadSentences={loadSentences}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Center: Details */}
|
||||
<main className="canvas-area" style={{ alignItems: 'flex-start', justifyContent: 'flex-start' }}>
|
||||
<div style={{ width: '100%', maxWidth: 520, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<div className="sidebar-panel" style={{ background: 'var(--surface)', borderRadius: 'var(--r-lg)', border: '1px solid var(--border)' }}>
|
||||
<DetailsPanel obj={selectedObj} objects={objects} sentences={sentences} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Right: Sentences */}
|
||||
<aside className="sidebar sidebar--right">
|
||||
<div className="sidebar-panel" style={{ flex: 1 }}>
|
||||
<h3 className="sidebar-heading">
|
||||
Alle Sätze
|
||||
{sentences.length > 0 && <span className="badge">{sentences.length}</span>}
|
||||
</h3>
|
||||
<SentencesList sentences={sentences} />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user