update
This commit is contained in:
986
frontend/src/components/ElementsSidebar.vue
Normal file
986
frontend/src/components/ElementsSidebar.vue
Normal file
@@ -0,0 +1,986 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { fetchElements, createElement, chatElement, deleteElement, updateElement, checkElement, styleElement } from '../api.js'
|
||||
import { renderMarkdown } from '../markdown.js'
|
||||
|
||||
const props = defineProps({
|
||||
topic: { type: String, required: true },
|
||||
provider: { type: String, default: 'claude' },
|
||||
openId: { type: String, default: null }, // Element-ID, die geöffnet werden soll
|
||||
openTick: { type: Number, default: 0 }, // Erhöhung = openId (erneut) öffnen
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'changed'])
|
||||
|
||||
const elements = ref([])
|
||||
const query = ref('')
|
||||
const creating = ref(false)
|
||||
const selected = ref(null)
|
||||
|
||||
watch(() => props.topic, load, { immediate: true })
|
||||
|
||||
async function load() {
|
||||
selected.value = null
|
||||
query.value = ''
|
||||
try {
|
||||
elements.value = await fetchElements(props.topic)
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Elemente:', e)
|
||||
}
|
||||
openFromProp()
|
||||
}
|
||||
|
||||
// Aus der Übersicht im Hauptbereich angeklicktes Element öffnen
|
||||
watch(() => props.openTick, openFromProp)
|
||||
|
||||
function openFromProp() {
|
||||
if (!props.openId) return
|
||||
const el = elements.value.find((e) => e.id === props.openId)
|
||||
if (el) select(el)
|
||||
}
|
||||
|
||||
// Markdown-Zeichen für Titel und Listen-Vorschau entfernen
|
||||
function plain(text) {
|
||||
return (text || '').replace(/```[a-z]*\n?/g, '').replace(/[`*_#]/g, '')
|
||||
}
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = query.value.trim().toLowerCase()
|
||||
if (!q) return elements.value
|
||||
return elements.value.filter(
|
||||
(el) => el.title.toLowerCase().includes(q) || el.description.toLowerCase().includes(q),
|
||||
)
|
||||
})
|
||||
|
||||
async function add() {
|
||||
if (creating.value) return
|
||||
creating.value = true
|
||||
try {
|
||||
const el = await createElement(props.topic, query.value.trim(), props.provider)
|
||||
elements.value.unshift(el)
|
||||
query.value = ''
|
||||
emit('changed')
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Erstellen des Elements:', e)
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function select(el) {
|
||||
selected.value = el
|
||||
messages.value = []
|
||||
input.value = ''
|
||||
resetCheck()
|
||||
nextTick(() => inputEl.value?.focus())
|
||||
}
|
||||
|
||||
function back() {
|
||||
selected.value = null
|
||||
messages.value = []
|
||||
resetCheck()
|
||||
}
|
||||
|
||||
// --- KI-Prüfung auf fehlende Infos (Ergebnisse landen als Inline-Vorschläge) ---
|
||||
const checking = ref(false)
|
||||
const statusMsg = ref(null)
|
||||
|
||||
function resetCheck() {
|
||||
checking.value = false
|
||||
statusMsg.value = null
|
||||
resetStyle()
|
||||
}
|
||||
|
||||
let checkRun = 0 // laufende Prüfung identifizieren; Abbruch ignoriert ihr Ergebnis
|
||||
|
||||
async function runCheck() {
|
||||
if (!selected.value) return
|
||||
if (checking.value) { // zweiter Klick = abbrechen
|
||||
checkRun++
|
||||
checking.value = false
|
||||
return
|
||||
}
|
||||
const run = ++checkRun
|
||||
checking.value = true
|
||||
statusMsg.value = null
|
||||
try {
|
||||
const res = await checkElement(selected.value.id, props.provider)
|
||||
if (run !== checkRun) return // abgebrochen oder neue Prüfung gestartet
|
||||
const mapped = res.suggestions.map((s) => ({
|
||||
text: s.text, action: 'hinzufuegen', target: s.target, index: null, content: s.content,
|
||||
}))
|
||||
if (mapped.length) styleChanges.value = [...(styleChanges.value || []), ...mapped]
|
||||
else statusMsg.value = 'Keine wichtigen Lücken gefunden.'
|
||||
} catch (e) {
|
||||
if (run !== checkRun) return
|
||||
console.error('Prüfung fehlgeschlagen:', e)
|
||||
statusMsg.value = 'Prüfung fehlgeschlagen — bitte erneut versuchen.'
|
||||
} finally {
|
||||
if (run === checkRun) checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Stil-Prüfung: KI schlägt Änderungen vor, Nutzer bestätigt ---
|
||||
const styleChanges = ref(null) // null = noch nicht geprüft
|
||||
const styling = ref(false)
|
||||
const applyingStyle = ref(false)
|
||||
const ACTION_LABELS = { entfernen: 'Entfernen:', anpassen: 'Anpassen:', hinzufuegen: 'Hinzufügen:' }
|
||||
let styleRun = 0
|
||||
|
||||
function resetStyle() {
|
||||
styleChanges.value = null
|
||||
styling.value = false
|
||||
applyingStyle.value = false
|
||||
}
|
||||
|
||||
async function runStyle() {
|
||||
if (!selected.value) return
|
||||
if (styling.value) { // zweiter Klick = abbrechen
|
||||
styleRun++
|
||||
styling.value = false
|
||||
return
|
||||
}
|
||||
const run = ++styleRun
|
||||
styling.value = true
|
||||
statusMsg.value = null
|
||||
try {
|
||||
const res = await styleElement(selected.value.id, props.provider)
|
||||
if (run !== styleRun) return
|
||||
if (res.changes.length) styleChanges.value = [...(styleChanges.value || []), ...res.changes]
|
||||
else statusMsg.value = 'Stil passt bereits.'
|
||||
} catch (e) {
|
||||
if (run !== styleRun) return
|
||||
console.error('Stil-Prüfung fehlgeschlagen:', e)
|
||||
statusMsg.value = 'Stil-Prüfung fehlgeschlagen — bitte erneut versuchen.'
|
||||
} finally {
|
||||
if (run === styleRun) styling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Vorschläge am Ziel-Ort anzeigen: anpassen/entfernen beim betroffenen Eintrag …
|
||||
function styleAt(target, index = null) {
|
||||
if (!styleChanges.value) return []
|
||||
return styleChanges.value
|
||||
.map((c, i) => [i, c])
|
||||
.filter(([, c]) => c.target === target && c.index === index && c.action !== 'hinzufuegen')
|
||||
}
|
||||
|
||||
// … Ergänzungen am Ende der jeweiligen Sektion
|
||||
function styleAdds(target) {
|
||||
if (!styleChanges.value) return []
|
||||
return styleChanges.value
|
||||
.map((c, i) => [i, c])
|
||||
.filter(([, c]) => c.target === target && c.action === 'hinzufuegen')
|
||||
}
|
||||
|
||||
function dismissStyleChange(i) {
|
||||
styleChanges.value = styleChanges.value.filter((_, j) => j !== i)
|
||||
}
|
||||
|
||||
async function applyStyleChange(i) {
|
||||
if (applyingStyle.value || !selected.value) return
|
||||
const c = styleChanges.value[i]
|
||||
applyingStyle.value = true
|
||||
try {
|
||||
const fields = {
|
||||
title: selected.value.title,
|
||||
description: selected.value.description,
|
||||
examples: [...selected.value.examples],
|
||||
hints: [...selected.value.hints],
|
||||
}
|
||||
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 fields[c.target].push(c.content)
|
||||
} else if (c.target === 'title' || c.target === 'description') fields[c.target] = c.content
|
||||
else fields[c.target][c.index] = c.content
|
||||
|
||||
const updated = await updateElement(selected.value.id, fields)
|
||||
selected.value = updated
|
||||
const idx = elements.value.findIndex((e) => e.id === updated.id)
|
||||
if (idx !== -1) elements.value.splice(idx, 1, updated)
|
||||
|
||||
// Rest-Vorschläge behalten; Indizes hinter einer Entfernung rücken auf
|
||||
const rest = styleChanges.value.filter((_, j) => j !== i)
|
||||
if (c.action === 'entfernen') {
|
||||
for (const r of rest) {
|
||||
if (r.target === c.target && r.index !== null && r.index > c.index) r.index--
|
||||
}
|
||||
}
|
||||
styleChanges.value = rest
|
||||
emit('changed')
|
||||
} catch (e) {
|
||||
console.error('Übernehmen fehlgeschlagen:', e)
|
||||
} finally {
|
||||
applyingStyle.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Inline-Bestätigung wie in TopicSidebar: erster Klick „Sicher?", zweiter löscht
|
||||
const pendingDelete = ref(null)
|
||||
let deleteTimer = null
|
||||
|
||||
async function confirmDelete(el) {
|
||||
clearTimeout(deleteTimer)
|
||||
if (pendingDelete.value === el.id) {
|
||||
pendingDelete.value = null
|
||||
await deleteElement(el.id)
|
||||
elements.value = elements.value.filter((e) => e.id !== el.id)
|
||||
if (selected.value?.id === el.id) back()
|
||||
emit('changed')
|
||||
} else {
|
||||
pendingDelete.value = el.id
|
||||
deleteTimer = setTimeout(() => { pendingDelete.value = null }, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Chat zum Anpassen des Elements ---
|
||||
const messages = ref([])
|
||||
const input = ref('')
|
||||
const loading = ref(false)
|
||||
const messagesEl = ref(null)
|
||||
const inputEl = ref(null)
|
||||
|
||||
async function scrollToBottom() {
|
||||
await nextTick()
|
||||
if (messagesEl.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const text = input.value.trim()
|
||||
if (!text || loading.value || !selected.value) return
|
||||
messages.value.push({ role: 'user', content: text })
|
||||
input.value = ''
|
||||
loading.value = true
|
||||
scrollToBottom()
|
||||
try {
|
||||
const res = await chatElement(selected.value.id, messages.value, props.provider)
|
||||
messages.value.push({ role: 'assistant', content: res.reply || '…' })
|
||||
if (res.element) {
|
||||
selected.value = res.element
|
||||
const i = elements.value.findIndex((e) => e.id === res.element.id)
|
||||
if (i !== -1) elements.value.splice(i, 1, res.element)
|
||||
emit('changed')
|
||||
}
|
||||
} catch {
|
||||
messages.value.push({ role: 'assistant', content: 'Fehler bei der Anfrage.' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
scrollToBottom()
|
||||
nextTick(() => inputEl.value?.focus())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="elements-sidebar">
|
||||
<header class="el-header">
|
||||
<button v-if="selected" class="el-back" title="Zur Liste" @click="back">←</button>
|
||||
<span class="el-title">{{ selected ? plain(selected.title) : 'Elemente' }}</span>
|
||||
<button
|
||||
v-if="selected" class="el-tool" :class="{ busy: checking }"
|
||||
:title="checking ? 'Prüfung abbrechen' : 'Auf fehlende Infos prüfen'" @click="runCheck"
|
||||
>🔍</button>
|
||||
<button
|
||||
v-if="selected" class="el-tool" :class="{ busy: styling }"
|
||||
:title="styling ? 'Prüfung abbrechen' : 'Stil prüfen & anpassen'" @click="runStyle"
|
||||
>✨</button>
|
||||
<button class="el-close" title="Schließen" @click="emit('close')">×</button>
|
||||
</header>
|
||||
|
||||
<!-- Listen-Modus -->
|
||||
<template v-if="!selected">
|
||||
<div class="el-new">
|
||||
<input
|
||||
v-model="query"
|
||||
placeholder="Suchen oder Stichwort…"
|
||||
:disabled="creating"
|
||||
@keyup.enter="add"
|
||||
/>
|
||||
<button :disabled="creating" title="Element per KI erstellen" @click="add">+</button>
|
||||
</div>
|
||||
<div v-if="creating" class="el-creating">KI erstellt Element…</div>
|
||||
<ul class="el-list">
|
||||
<li v-for="el in filtered" :key="el.id" @click="select(el)">
|
||||
<div class="el-item-main">
|
||||
<span class="el-item-title">{{ plain(el.title) }}</span>
|
||||
<span class="el-item-desc">{{ plain(el.description) }}</span>
|
||||
</div>
|
||||
<button
|
||||
class="el-delete"
|
||||
:class="{ armed: pendingDelete === el.id }"
|
||||
title="Element löschen"
|
||||
@click.stop="confirmDelete(el)"
|
||||
>{{ pendingDelete === el.id ? 'Sicher?' : '×' }}</button>
|
||||
</li>
|
||||
<li v-if="!filtered.length && !creating" class="el-empty">
|
||||
{{ elements.length ? 'Keine Treffer.' : 'Noch keine Elemente. Stichwort eingeben und + klicken.' }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<!-- Detail-Modus -->
|
||||
<template v-else>
|
||||
<div class="el-detail">
|
||||
<div v-if="selected.description" class="el-desc markdown" v-html="renderMarkdown(selected.description)"></div>
|
||||
<div
|
||||
v-for="[ci, c] in [...styleAt('title'), ...styleAt('description'), ...styleAdds('description')]"
|
||||
:key="'sgd' + ci" class="style-sugg"
|
||||
>
|
||||
<div class="style-sugg-text"><strong>{{ ACTION_LABELS[c.action] }}</strong> {{ c.text }}</div>
|
||||
<div v-if="c.content" class="style-sugg-preview markdown" v-html="renderMarkdown(c.content)"></div>
|
||||
<div class="style-sugg-actions">
|
||||
<button class="sugg-ok" :disabled="applyingStyle" @click="applyStyleChange(ci)">Bestätigen</button>
|
||||
<button class="sugg-no" :disabled="applyingStyle" @click="dismissStyleChange(ci)">Ablehnen</button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="(ex, i) in selected.examples" :key="i">
|
||||
<div class="el-entry markdown" v-html="renderMarkdown(ex)"></div>
|
||||
<div v-for="[ci, c] in styleAt('examples', i)" :key="'sge' + ci" class="style-sugg">
|
||||
<div class="style-sugg-text"><strong>{{ ACTION_LABELS[c.action] }}</strong> {{ c.text }}</div>
|
||||
<div v-if="c.content" class="style-sugg-preview markdown" v-html="renderMarkdown(c.content)"></div>
|
||||
<div class="style-sugg-actions">
|
||||
<button class="sugg-ok" :disabled="applyingStyle" @click="applyStyleChange(ci)">Bestätigen</button>
|
||||
<button class="sugg-no" :disabled="applyingStyle" @click="dismissStyleChange(ci)">Ablehnen</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-for="[ci, c] in styleAdds('examples')" :key="'sgea' + ci" class="style-sugg">
|
||||
<div class="style-sugg-text"><strong>{{ ACTION_LABELS[c.action] }}</strong> {{ c.text }}</div>
|
||||
<div v-if="c.content" class="style-sugg-preview markdown" v-html="renderMarkdown(c.content)"></div>
|
||||
<div class="style-sugg-actions">
|
||||
<button class="sugg-ok" :disabled="applyingStyle" @click="applyStyleChange(ci)">Bestätigen</button>
|
||||
<button class="sugg-no" :disabled="applyingStyle" @click="dismissStyleChange(ci)">Ablehnen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selected.hints.length || styleAdds('hints').length" class="el-hints-block">
|
||||
<h4>Hinweise</h4>
|
||||
<ul class="el-hints">
|
||||
<li v-for="(h, i) in selected.hints" :key="i">
|
||||
<span class="markdown" v-html="renderMarkdown(h)"></span>
|
||||
<div v-for="[ci, c] in styleAt('hints', i)" :key="'sgh' + ci" class="style-sugg">
|
||||
<div class="style-sugg-text"><strong>{{ ACTION_LABELS[c.action] }}</strong> {{ c.text }}</div>
|
||||
<div v-if="c.content" class="style-sugg-preview markdown" v-html="renderMarkdown(c.content)"></div>
|
||||
<div class="style-sugg-actions">
|
||||
<button class="sugg-ok" :disabled="applyingStyle" @click="applyStyleChange(ci)">Bestätigen</button>
|
||||
<button class="sugg-no" :disabled="applyingStyle" @click="dismissStyleChange(ci)">Ablehnen</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-for="[ci, c] in styleAdds('hints')" :key="'sgha' + ci" class="style-sugg">
|
||||
<div class="style-sugg-text"><strong>{{ ACTION_LABELS[c.action] }}</strong> {{ c.text }}</div>
|
||||
<div v-if="c.content" class="style-sugg-preview markdown" v-html="renderMarkdown(c.content)"></div>
|
||||
<div class="style-sugg-actions">
|
||||
<button class="sugg-ok" :disabled="applyingStyle" @click="applyStyleChange(ci)">Bestätigen</button>
|
||||
<button class="sugg-no" :disabled="applyingStyle" @click="dismissStyleChange(ci)">Ablehnen</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 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">
|
||||
<div :class="['chat-msg', m.role]">{{ m.content }}</div>
|
||||
</template>
|
||||
<div v-if="loading" class="chat-msg assistant chat-typing">Passt an…</div>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<textarea
|
||||
ref="inputEl"
|
||||
v-model="input"
|
||||
placeholder="Element anpassen…"
|
||||
@keydown.enter.exact.prevent="send"
|
||||
></textarea>
|
||||
<button :disabled="!input.trim() || loading" @click="send">➤</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.elements-sidebar {
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
border-left: 1px solid var(--border);
|
||||
/* Über dem Guide-Chat (FAB/Panel: z-index 20) */
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.el-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.el-title {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-back,
|
||||
.el-close {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-faint);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.el-back:hover,
|
||||
.el-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.el-tool {
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 2px 3px;
|
||||
border-radius: 6px;
|
||||
filter: grayscale(0.4);
|
||||
}
|
||||
|
||||
.el-tool:hover {
|
||||
background: var(--panel-soft);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.el-tool.busy {
|
||||
filter: none;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.busy-text {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.el-new {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 0.6rem 0.75rem;
|
||||
}
|
||||
|
||||
.el-new input {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.el-new input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.el-new button {
|
||||
width: 38px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-new button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.el-creating {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--warning);
|
||||
background: var(--warning-soft);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
.el-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.el-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.el-list li:hover {
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.el-item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.el-item-title {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-item-desc {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-faint);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-delete {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--danger);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.el-list li:hover .el-delete {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.el-delete.armed {
|
||||
visibility: visible;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.el-empty {
|
||||
cursor: default !important;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.el-empty:hover {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.el-detail {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.el-desc {
|
||||
margin: 0 0 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.el-hints-block {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.el-hints-block h4 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.el-entry {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.el-entry:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.el-hints {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
|
||||
.el-hints li {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Hinweis-Text inline neben dem Bullet halten (p ist sonst block) */
|
||||
.el-hints li > .markdown {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.el-hints li > .markdown :deep(p) {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- Markdown im Guide-Stil (Muster: TopicDetail) --- */
|
||||
.markdown :deep(p) {
|
||||
margin: 0 0 0.5em;
|
||||
}
|
||||
|
||||
.markdown :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown :deep(ul),
|
||||
.markdown :deep(ol) {
|
||||
margin: 0.3em 0;
|
||||
padding-left: 1.2em;
|
||||
}
|
||||
|
||||
.markdown :deep(code) {
|
||||
background: var(--border);
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: "SF Mono", Consolas, monospace;
|
||||
font-size: 0.85em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.markdown :deep(pre) {
|
||||
background: var(--code-bg, #1e2330);
|
||||
color: var(--code-fg, #e6e8ee);
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
/* Schmale Sidebar: Code umbrechen statt horizontal scrollen */
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.markdown :deep(pre code) {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* --- Stil-Vorschläge inline am Ziel --- */
|
||||
.style-sugg {
|
||||
margin: 0.3rem 0 0.6rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
border: 1px dashed var(--accent);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.style-sugg-text {
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.4;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.style-sugg-text strong {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.style-sugg-preview {
|
||||
margin-top: 0.35rem;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.style-sugg-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
|
||||
.sugg-ok,
|
||||
.sugg-no {
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sugg-ok {
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.sugg-no {
|
||||
border: 1px solid var(--border-strong);
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sugg-no:hover {
|
||||
border-color: var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.sugg-ok:disabled,
|
||||
.sugg-no:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- KI-Prüfung --- */
|
||||
.el-check {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.8rem;
|
||||
border-top: 1px dashed var(--border-strong);
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 0.4rem;
|
||||
padding: 7px 10px;
|
||||
border: 1.5px dashed var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-soft);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
|
||||
.check-btn:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.check-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.check-empty {
|
||||
margin: 0.6rem 0 0;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-faint);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sugg-list {
|
||||
list-style: none;
|
||||
margin: 0.6rem 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sugg-list li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
padding: 0.4rem 0.2rem;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.sugg-list li:hover {
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.sugg-list input {
|
||||
margin-top: 2px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sugg-text em {
|
||||
color: var(--text-faint);
|
||||
font-style: normal;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.check-btn.busy {
|
||||
border-style: solid;
|
||||
border-color: var(--warning);
|
||||
color: var(--warning);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sugg-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dismiss-btn {
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dismiss-btn:hover {
|
||||
border-color: var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
flex: 1;
|
||||
padding: 7px 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.apply-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Chat unten (Muster: TopicDetail-Chat) */
|
||||
.el-chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 33%;
|
||||
min-height: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.6rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-hint {
|
||||
color: var(--text-faint);
|
||||
font-size: 0.78rem;
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
max-width: 85%;
|
||||
padding: 6px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chat-msg.user {
|
||||
align-self: flex-end;
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-msg.assistant {
|
||||
align-self: flex-start;
|
||||
background: var(--panel-soft);
|
||||
color: var(--text);
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-typing {
|
||||
color: var(--text-faint);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 0.6rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.chat-input textarea {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
height: 72px;
|
||||
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;
|
||||
}
|
||||
|
||||
.chat-input textarea:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.chat-input button {
|
||||
width: 38px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-input button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user