This commit is contained in:
team3
2026-06-07 16:34:17 +02:00
parent ab8c577899
commit 58fd209174
8 changed files with 437 additions and 189 deletions

View File

@@ -0,0 +1,212 @@
<script setup>
import { ref, nextTick } from 'vue'
import { renderMarkdown } from '../markdown.js'
const props = defineProps({
change: { type: Object, required: true },
busy: { type: Boolean, default: false },
})
const emit = defineEmits(['apply', 'dismiss', 'refine'])
const ACTION_LABELS = { entfernen: 'Entfernen:', anpassen: 'Anpassen:', hinzufuegen: 'Hinzufügen:' }
const editing = ref(false)
const instruction = ref('')
const inputEl = ref(null)
function toggleEdit() {
editing.value = !editing.value
if (editing.value) nextTick(() => inputEl.value?.focus())
}
function submit() {
const text = instruction.value.trim()
if (!text || props.busy) return
emit('refine', text)
instruction.value = ''
editing.value = false
}
</script>
<template>
<div class="style-sugg" :class="{ busy }">
<div class="style-sugg-text"><strong>{{ ACTION_LABELS[change.action] }}</strong> {{ change.text }}</div>
<div v-if="change.content" class="style-sugg-preview markdown" v-html="renderMarkdown(change.content)"></div>
<div class="style-sugg-actions">
<button class="sugg-ok" :disabled="busy" @click="emit('apply')">Bestätigen</button>
<button class="sugg-no" :disabled="busy" @click="emit('dismiss')">Ablehnen</button>
<button class="sugg-edit" :disabled="busy" title="Vorschlag per Anweisung anpassen" @click="toggleEdit"></button>
</div>
<div v-if="editing" class="sugg-edit-row">
<input
ref="inputEl"
v-model="instruction"
placeholder="Anweisung zum Vorschlag…"
@keyup.enter="submit"
/>
<button :disabled="!instruction.trim() || busy" @click="submit"></button>
</div>
</div>
</template>
<style scoped>
.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.busy {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
50% { opacity: 0.45; }
}
.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;
align-items: center;
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-edit {
border: none;
background: none;
font-size: 0.8rem;
cursor: pointer;
padding: 2px 4px;
border-radius: 6px;
filter: grayscale(0.4);
}
.sugg-edit:hover {
background: var(--border);
filter: none;
}
.sugg-ok:disabled,
.sugg-no:disabled,
.sugg-edit:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.sugg-edit-row {
display: flex;
gap: 6px;
margin-top: 0.45rem;
}
.sugg-edit-row input {
flex: 1;
padding: 5px 8px;
border: 1px solid var(--border-strong);
border-radius: 6px;
font-size: 0.76rem;
background: var(--panel);
color: var(--text);
outline: none;
}
.sugg-edit-row input:focus {
border-color: var(--accent);
}
.sugg-edit-row button {
width: 30px;
border: none;
border-radius: 6px;
background: var(--accent);
color: var(--on-accent);
font-size: 0.8rem;
cursor: pointer;
}
.sugg-edit-row button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Markdown in der Vorschau */
.markdown :deep(p) {
margin: 0 0 0.4em;
}
.markdown :deep(p:last-child) {
margin-bottom: 0;
}
.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: 6px 8px;
border-radius: 6px;
white-space: pre-wrap;
overflow-wrap: anywhere;
margin: 0.3em 0;
}
.markdown :deep(pre code) {
background: none;
padding: 0;
color: inherit;
font-size: 0.85em;
}
</style>

View File

@@ -1,7 +1,8 @@
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { fetchElements, createElement, chatElement, deleteElement, updateElement, checkElement, styleElement } from '../api.js'
import { fetchElements, createElement, chatElement, deleteElement, updateElement, checkElement, styleElement, refineSuggestion } from '../api.js'
import { renderMarkdown } from '../markdown.js'
import ElementSuggestion from './ElementSuggestion.vue'
const props = defineProps({
topic: { type: String, required: true },
@@ -124,13 +125,35 @@ async function runCheck() {
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:' }
const refiningIdx = ref(null)
let styleRun = 0
function resetStyle() {
styleChanges.value = null
styling.value = false
applyingStyle.value = false
refiningIdx.value = null
}
function suggBusy(i) {
return applyingStyle.value || refiningIdx.value === i
}
// Einzelnen Vorschlag per Anweisung überarbeiten (Stift-Icon)
async function refineChange(i, instruction) {
if (refiningIdx.value !== null || applyingStyle.value || !selected.value) return
refiningIdx.value = i
try {
const res = await refineSuggestion(selected.value.id, styleChanges.value[i], instruction, props.provider)
const next = [...styleChanges.value]
next[i] = res.change
styleChanges.value = next
} catch (e) {
console.error('Überarbeitung fehlgeschlagen:', e)
statusMsg.value = 'Überarbeitung fehlgeschlagen — bitte erneut versuchen.'
} finally {
refiningIdx.value = null
}
}
async function runStyle() {
@@ -247,28 +270,38 @@ async function scrollToBottom() {
if (messagesEl.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight
}
let chatRun = 0 // laufende Anfrage identifizieren; Abbruch ignoriert ihr Ergebnis
async function send() {
if (loading.value) { // zweiter Klick = abbrechen
chatRun++
loading.value = false
messages.value.push({ role: 'assistant', content: 'Abgebrochen.' })
return
}
const text = input.value.trim()
if (!text || loading.value || !selected.value) return
if (!text || !selected.value) return
const run = ++chatRun
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)
if (run !== chatRun) return
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')
if (res.changes?.length) {
styleChanges.value = [...(styleChanges.value || []), ...res.changes]
}
} catch {
if (run !== chatRun) return
messages.value.push({ role: 'assistant', content: 'Fehler bei der Anfrage.' })
} finally {
loading.value = false
scrollToBottom()
nextTick(() => inputEl.value?.focus())
if (run === chatRun) {
loading.value = false
scrollToBottom()
nextTick(() => inputEl.value?.focus())
}
}
}
</script>
@@ -324,59 +357,41 @@ async function send() {
<template v-else>
<div class="el-detail">
<div v-if="selected.description" class="el-desc markdown" v-html="renderMarkdown(selected.description)"></div>
<div
<ElementSuggestion
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>
:key="'sgd' + ci" :change="c" :busy="suggBusy(ci)"
@apply="applyStyleChange(ci)" @dismiss="dismissStyleChange(ci)" @refine="(t) => refineChange(ci, t)"
/>
<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>
<ElementSuggestion
v-for="[ci, c] in styleAt('examples', i)"
:key="'sge' + ci" :change="c" :busy="suggBusy(ci)"
@apply="applyStyleChange(ci)" @dismiss="dismissStyleChange(ci)" @refine="(t) => refineChange(ci, t)"
/>
</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>
<ElementSuggestion
v-for="[ci, c] in styleAdds('examples')"
:key="'sgea' + ci" :change="c" :busy="suggBusy(ci)"
@apply="applyStyleChange(ci)" @dismiss="dismissStyleChange(ci)" @refine="(t) => refineChange(ci, t)"
/>
<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>
<ElementSuggestion
v-for="[ci, c] in styleAt('hints', i)"
:key="'sgh' + ci" :change="c" :busy="suggBusy(ci)"
@apply="applyStyleChange(ci)" @dismiss="dismissStyleChange(ci)" @refine="(t) => refineChange(ci, t)"
/>
</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>
<ElementSuggestion
v-for="[ci, c] in styleAdds('hints')"
:key="'sgha' + 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">
@@ -400,7 +415,12 @@ async function send() {
placeholder="Element anpassen…"
@keydown.enter.exact.prevent="send"
></textarea>
<button :disabled="!input.trim() || loading" @click="send"></button>
<button
:disabled="!input.trim() && !loading"
:class="{ cancel: loading }"
:title="loading ? 'Abbrechen' : 'Senden'"
@click="send"
>{{ loading ? '✕' : '➤' }}</button>
</div>
</div>
</template>
@@ -709,70 +729,6 @@ async function send() {
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;
@@ -983,4 +939,8 @@ async function send() {
opacity: 0.4;
cursor: not-allowed;
}
.chat-input button.cancel {
background: var(--danger);
}
</style>