update
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
|
||||
<title>Creator</title>
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
@@ -259,7 +259,7 @@ function handlePreview(guide) {
|
||||
function handleOpenElements() {
|
||||
if (!selectedTopic.value) return
|
||||
elementsView.value = true
|
||||
elementsOpen.value = true
|
||||
// Rechte Sidebar bleibt zu — sie öffnet erst beim Klick auf ein Element.
|
||||
}
|
||||
|
||||
function handleOpenElementDetail(el) {
|
||||
@@ -398,6 +398,11 @@ onUnmounted(() => {
|
||||
<div v-else class="empty-main">
|
||||
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="elementsOpen && selectedTopic"
|
||||
class="elements-backdrop"
|
||||
@click="elementsOpen = false"
|
||||
></div>
|
||||
<ElementsSidebar
|
||||
v-if="elementsOpen && selectedTopic"
|
||||
:topic="selectedTopic"
|
||||
@@ -490,7 +495,7 @@ textarea::placeholder {
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
@@ -500,7 +505,7 @@ textarea::placeholder {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 50px;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
z-index: 5;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -518,7 +523,7 @@ textarea::placeholder {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
z-index: 10;
|
||||
@@ -539,4 +544,20 @@ textarea::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Nur sichtbar, wenn die Elemente-Sidebar mobil als Overlay liegt.
|
||||
Tipp daneben schließt sie. */
|
||||
.elements-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.elements-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 29;
|
||||
background: var(--shadow);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,9 @@ const elements = ref([])
|
||||
const query = ref('')
|
||||
const creating = ref(false)
|
||||
const selected = ref(null)
|
||||
const tab = ref('overview') // 'overview' | 'chat' | 'edit'
|
||||
const edit = ref({ title: '', description: '', examples: [], hints: [], aufgabe: '', loesung: '' })
|
||||
const savingEdit = ref(false)
|
||||
|
||||
watch(() => props.topic, load, { immediate: true })
|
||||
|
||||
@@ -70,10 +73,10 @@ async function add() {
|
||||
|
||||
function select(el) {
|
||||
selected.value = el
|
||||
tab.value = 'overview'
|
||||
messages.value = []
|
||||
input.value = ''
|
||||
resetCheck()
|
||||
nextTick(() => inputEl.value?.focus())
|
||||
}
|
||||
|
||||
function back() {
|
||||
@@ -82,6 +85,48 @@ function back() {
|
||||
resetCheck()
|
||||
}
|
||||
|
||||
// --- Bearbeiten-Tab: Felder direkt editieren ---
|
||||
function loadEdit() {
|
||||
if (!selected.value) return
|
||||
edit.value = {
|
||||
title: selected.value.title,
|
||||
description: selected.value.description,
|
||||
examples: [...selected.value.examples],
|
||||
hints: [...selected.value.hints],
|
||||
aufgabe: selected.value.aufgabe || '',
|
||||
loesung: selected.value.loesung || '',
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit() {
|
||||
loadEdit()
|
||||
tab.value = 'edit'
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!selected.value || savingEdit.value) return
|
||||
savingEdit.value = true
|
||||
try {
|
||||
const updated = await updateElement(selected.value.id, {
|
||||
title: edit.value.title,
|
||||
description: edit.value.description,
|
||||
examples: edit.value.examples.filter((s) => s.trim()),
|
||||
hints: edit.value.hints.filter((s) => s.trim()),
|
||||
aufgabe: edit.value.aufgabe,
|
||||
loesung: edit.value.loesung,
|
||||
})
|
||||
selected.value = updated
|
||||
const idx = elements.value.findIndex((e) => e.id === updated.id)
|
||||
if (idx !== -1) elements.value.splice(idx, 1, updated)
|
||||
emit('changed')
|
||||
tab.value = 'overview'
|
||||
} catch (e) {
|
||||
console.error('Speichern fehlgeschlagen:', e)
|
||||
} finally {
|
||||
savingEdit.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- KI-Prüfung auf fehlende Infos (Ergebnisse landen als Inline-Vorschläge) ---
|
||||
const checking = ref(false)
|
||||
const statusMsg = ref(null)
|
||||
@@ -205,18 +250,22 @@ async function applyStyleChange(i) {
|
||||
const c = styleChanges.value[i]
|
||||
applyingStyle.value = true
|
||||
try {
|
||||
const STRING_TARGETS = ['title', 'description', 'aufgabe', 'loesung']
|
||||
const fields = {
|
||||
title: selected.value.title,
|
||||
description: selected.value.description,
|
||||
examples: [...selected.value.examples],
|
||||
hints: [...selected.value.hints],
|
||||
aufgabe: selected.value.aufgabe || '',
|
||||
loesung: selected.value.loesung || '',
|
||||
}
|
||||
if (c.action === 'entfernen') fields[c.target].splice(c.index, 1)
|
||||
else if (c.action === 'hinzufuegen') {
|
||||
if (c.target === 'title') fields.title = c.content
|
||||
else if (c.target === 'description') fields.description += '\n\n' + c.content
|
||||
else if (c.target === 'description' || c.target === 'aufgabe' || c.target === 'loesung')
|
||||
fields[c.target] = fields[c.target] ? fields[c.target] + '\n\n' + c.content : c.content
|
||||
else fields[c.target].push(c.content)
|
||||
} else if (c.target === 'title' || c.target === 'description') fields[c.target] = c.content
|
||||
} else if (STRING_TARGETS.includes(c.target)) fields[c.target] = c.content
|
||||
else fields[c.target][c.index] = c.content
|
||||
|
||||
const updated = await updateElement(selected.value.id, fields)
|
||||
@@ -355,7 +404,12 @@ async function send() {
|
||||
|
||||
<!-- Detail-Modus -->
|
||||
<template v-else>
|
||||
<div class="el-detail">
|
||||
<nav class="el-tabs">
|
||||
<button :class="{ active: tab === 'overview' }" @click="tab = 'overview'">Übersicht</button>
|
||||
<button :class="{ active: tab === 'chat' }" @click="tab = 'chat'">Chat</button>
|
||||
<button :class="{ active: tab === 'edit' }" @click="openEdit">Bearbeiten</button>
|
||||
</nav>
|
||||
<div v-show="tab === 'overview'" class="el-detail">
|
||||
<div v-if="selected.description" class="el-desc markdown" v-html="renderMarkdown(selected.description)"></div>
|
||||
<ElementSuggestion
|
||||
v-for="[ci, c] in [...styleAt('title'), ...styleAt('description'), ...styleAdds('description')]"
|
||||
@@ -394,13 +448,32 @@ async function send() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="selected.aufgabe || styleAt('aufgabe').length || styleAdds('aufgabe').length" class="el-task-block">
|
||||
<h4>Aufgabe</h4>
|
||||
<div v-if="selected.aufgabe" class="el-entry markdown" v-html="renderMarkdown(selected.aufgabe)"></div>
|
||||
<ElementSuggestion
|
||||
v-for="[ci, c] in [...styleAt('aufgabe'), ...styleAdds('aufgabe')]"
|
||||
:key="'sga' + ci" :change="c" :busy="suggBusy(ci)"
|
||||
@apply="applyStyleChange(ci)" @dismiss="dismissStyleChange(ci)" @refine="(t) => refineChange(ci, t)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="selected.loesung || styleAt('loesung').length || styleAdds('loesung').length" class="el-task-block">
|
||||
<h4>Lösung</h4>
|
||||
<div v-if="selected.loesung" class="el-entry markdown" v-html="renderMarkdown(selected.loesung)"></div>
|
||||
<ElementSuggestion
|
||||
v-for="[ci, c] in [...styleAt('loesung'), ...styleAdds('loesung')]"
|
||||
:key="'sgl' + ci" :change="c" :busy="suggBusy(ci)"
|
||||
@apply="applyStyleChange(ci)" @dismiss="dismissStyleChange(ci)" @refine="(t) => refineChange(ci, t)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="checking || styling || statusMsg" class="el-check">
|
||||
<p v-if="checking" class="check-empty busy-text">Prüft auf fehlende Infos…</p>
|
||||
<p v-if="styling" class="check-empty busy-text">Prüft den Stil…</p>
|
||||
<p v-if="statusMsg && !checking && !styling" class="check-empty">{{ statusMsg }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="el-chat">
|
||||
<div v-show="tab === 'chat'" class="el-chat">
|
||||
<div ref="messagesEl" class="chat-messages">
|
||||
<p v-if="!messages.length" class="chat-hint">Schreib, was am Element geändert werden soll.</p>
|
||||
<template v-for="(m, i) in messages" :key="i">
|
||||
@@ -423,6 +496,39 @@ async function send() {
|
||||
>{{ loading ? '✕' : '➤' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bearbeiten-Modus: Felder direkt editieren -->
|
||||
<div v-show="tab === 'edit'" class="el-edit">
|
||||
<button class="edit-save" :disabled="savingEdit" @click="saveEdit">
|
||||
{{ savingEdit ? 'Speichert…' : 'Speichern' }}
|
||||
</button>
|
||||
|
||||
<label>Titel</label>
|
||||
<input v-model="edit.title" placeholder="Titel" />
|
||||
|
||||
<label>Beschreibung</label>
|
||||
<textarea v-model="edit.description" placeholder="Beschreibung"></textarea>
|
||||
|
||||
<label>Beispiele</label>
|
||||
<div v-for="(ex, i) in edit.examples" :key="'ex' + i" class="edit-row">
|
||||
<textarea v-model="edit.examples[i]" placeholder="Beispiel"></textarea>
|
||||
<button class="edit-del" title="Entfernen" @click="edit.examples.splice(i, 1)">×</button>
|
||||
</div>
|
||||
<button class="edit-add" @click="edit.examples.push('')">+ Beispiel</button>
|
||||
|
||||
<label>Hinweise</label>
|
||||
<div v-for="(h, i) in edit.hints" :key="'hi' + i" class="edit-row">
|
||||
<textarea v-model="edit.hints[i]" placeholder="Hinweis"></textarea>
|
||||
<button class="edit-del" title="Entfernen" @click="edit.hints.splice(i, 1)">×</button>
|
||||
</div>
|
||||
<button class="edit-add" @click="edit.hints.push('')">+ Hinweis</button>
|
||||
|
||||
<label>Aufgabenstellung</label>
|
||||
<textarea v-model="edit.aufgabe" placeholder="Aufgabenstellung"></textarea>
|
||||
|
||||
<label>Aufgabenlösung</label>
|
||||
<textarea v-model="edit.loesung" placeholder="Aufgabenlösung"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -431,7 +537,7 @@ async function send() {
|
||||
.elements-sidebar {
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
@@ -441,6 +547,19 @@ async function send() {
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
/* Mobil/schmal: als Overlay über den Hauptinhalt legen, statt ihn
|
||||
im Flex-Fluss einzuquetschen. */
|
||||
@media (max-width: 768px) {
|
||||
.elements-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: min(100vw, 380px);
|
||||
min-width: 0;
|
||||
box-shadow: -4px 0 16px var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.el-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -500,6 +619,32 @@ async function send() {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.el-tabs button {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.25rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.el-tabs button.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.el-tabs button:hover:not(.active) {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.el-new {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
@@ -641,11 +786,13 @@ async function send() {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.el-hints-block {
|
||||
.el-hints-block,
|
||||
.el-task-block {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.el-hints-block h4 {
|
||||
.el-hints-block h4,
|
||||
.el-task-block h4 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
@@ -846,13 +993,114 @@ async function send() {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Chat unten (Muster: TopicDetail-Chat) */
|
||||
/* Chat-Tab (Muster: TopicDetail-Chat) */
|
||||
.el-chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 33%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* --- Bearbeiten-Tab --- */
|
||||
.el-edit {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.9rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.el-edit label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-faint);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.el-edit input,
|
||||
.el-edit textarea {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.el-edit textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
overflow: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.el-edit input:focus,
|
||||
.el-edit textarea:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.edit-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.edit-row textarea {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.edit-del {
|
||||
flex-shrink: 0;
|
||||
width: 30px;
|
||||
align-self: stretch;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
color: var(--danger);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-add {
|
||||
align-self: flex-start;
|
||||
padding: 5px 10px;
|
||||
border: 1px dashed var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-add:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.edit-save {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
margin-bottom: 0.3rem;
|
||||
padding: 9px 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-save:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
|
||||
@@ -146,11 +146,19 @@ async function scrollToBottom() {
|
||||
if (messagesEl.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight
|
||||
}
|
||||
|
||||
function autoGrow() {
|
||||
const el = inputEl.value
|
||||
if (!el) return
|
||||
el.style.height = 'auto'
|
||||
el.style.height = Math.min(el.scrollHeight, 140) + 'px'
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const text = input.value.trim()
|
||||
if (!text || loading.value || !props.previewGuide) return
|
||||
messages.value.push({ role: 'user', content: text })
|
||||
input.value = ''
|
||||
nextTick(autoGrow)
|
||||
loading.value = true
|
||||
scrollToBottom()
|
||||
try {
|
||||
@@ -237,7 +245,9 @@ async function send() {
|
||||
<textarea
|
||||
ref="inputEl"
|
||||
v-model="input"
|
||||
rows="3"
|
||||
placeholder="Frage stellen…"
|
||||
@input="autoGrow"
|
||||
@keydown.enter.exact.prevent="send"
|
||||
></textarea>
|
||||
<button :disabled="!input.trim() || loading" @click="send">➤</button>
|
||||
@@ -252,7 +262,7 @@ async function send() {
|
||||
/* Flex-Item darf schmaler werden als seine Code-Blöcke — sonst sprengt
|
||||
deren Mindestbreite auf Mobile das Layout */
|
||||
min-width: 0;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -269,7 +279,7 @@ async function send() {
|
||||
margin: 0 auto;
|
||||
padding: 2rem 2.5rem 5rem;
|
||||
/* Lese-Zoom nur für den Inhalt — Sidebar/Chat bleiben unverändert */
|
||||
zoom: 1.2;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@@ -623,7 +633,7 @@ async function send() {
|
||||
bottom: 1.5rem;
|
||||
width: 360px;
|
||||
height: 500px;
|
||||
max-height: calc(100vh - 3rem);
|
||||
max-height: calc(100dvh - 3rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
@@ -706,6 +716,7 @@ async function send() {
|
||||
|
||||
.chat-input {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
padding: 0.6rem;
|
||||
border-top: 1px solid var(--border);
|
||||
@@ -714,12 +725,15 @@ async function send() {
|
||||
.chat-input textarea {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
height: 38px;
|
||||
min-height: 72px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
line-height: 1.4;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -729,6 +743,7 @@ async function send() {
|
||||
|
||||
.chat-input button {
|
||||
width: 38px;
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
|
||||
@@ -26,7 +26,7 @@ function providerAvailable(id) {
|
||||
return p ? p.available : true
|
||||
}
|
||||
|
||||
const PROVIDER_LABELS = { claude: 'Claude', minimax: 'MiniMax', lokal: 'Lokal' }
|
||||
const PROVIDER_LABELS = { claude: 'Claude', minimax: 'MiniMax', 'minimax-direkt': 'MiniMax direkt', lokal: 'Lokal' }
|
||||
|
||||
// Tracker oben in der Navigation: Themen gesamt, pro Format erstellt/absolviert
|
||||
const trackerItems = computed(() => {
|
||||
@@ -385,7 +385,7 @@ function confirmDeleteProject(name) {
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.new-topic {
|
||||
|
||||
Reference in New Issue
Block a user