update
This commit is contained in:
@@ -53,6 +53,16 @@ PROVIDERS = {
|
|||||||
"quick": "minimax/MiniMax-M2.7-highspeed",
|
"quick": "minimax/MiniMax-M2.7-highspeed",
|
||||||
"env_key": "MINIMAX_API_KEY",
|
"env_key": "MINIMAX_API_KEY",
|
||||||
},
|
},
|
||||||
|
# Wie "minimax", aber Chat/Elemente (Rolle "fast") laufen auf M3 OHNE Thinking.
|
||||||
|
# M2.x kann Thinking nicht abschalten — nur M3 respektiert thinking:disabled.
|
||||||
|
# guide/quick bleiben identisch zur Thinking-Variante.
|
||||||
|
"minimax-direkt": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"guide": "minimax/MiniMax-M3",
|
||||||
|
"fast": "minimax-direkt/MiniMax-M3",
|
||||||
|
"quick": "minimax/MiniMax-M2.7-highspeed",
|
||||||
|
"env_key": "MINIMAX_API_KEY",
|
||||||
|
},
|
||||||
"lokal": {
|
"lokal": {
|
||||||
"cli": "opencode",
|
"cli": "opencode",
|
||||||
"guide": "ollama/qwen3.6:27b",
|
"guide": "ollama/qwen3.6:27b",
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ CREATE TABLE IF NOT EXISTS elements (
|
|||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
examples TEXT NOT NULL DEFAULT '[]',
|
examples TEXT NOT NULL DEFAULT '[]',
|
||||||
hints TEXT NOT NULL DEFAULT '[]',
|
hints TEXT NOT NULL DEFAULT '[]',
|
||||||
|
aufgabe TEXT NOT NULL DEFAULT '',
|
||||||
|
loesung TEXT NOT NULL DEFAULT '',
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
)
|
)
|
||||||
@@ -68,6 +70,11 @@ async def init_db():
|
|||||||
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
|
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
|
||||||
except aiosqlite.OperationalError:
|
except aiosqlite.OperationalError:
|
||||||
pass
|
pass
|
||||||
|
for col in ("aufgabe", "loesung"): # Migration für Elemente ohne Aufgabe/Lösung
|
||||||
|
try:
|
||||||
|
await db.execute(f"ALTER TABLE elements ADD COLUMN {col} TEXT NOT NULL DEFAULT ''")
|
||||||
|
except aiosqlite.OperationalError:
|
||||||
|
pass
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' "
|
"UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' "
|
||||||
"WHERE status IN ('queued', 'generating')"
|
"WHERE status IN ('queued', 'generating')"
|
||||||
@@ -166,8 +173,8 @@ def _element_row(row, cursor) -> dict:
|
|||||||
async def create_element(element: dict) -> dict:
|
async def create_element(element: dict) -> dict:
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""INSERT INTO elements (id, topic, title, description, examples, hints, created_at, updated_at)
|
"""INSERT INTO elements (id, topic, title, description, examples, hints, aufgabe, loesung, created_at, updated_at)
|
||||||
VALUES (:id, :topic, :title, :description, :examples, :hints, :created_at, :updated_at)""",
|
VALUES (:id, :topic, :title, :description, :examples, :hints, :aufgabe, :loesung, :created_at, :updated_at)""",
|
||||||
{**element, "examples": json.dumps(element["examples"], ensure_ascii=False),
|
{**element, "examples": json.dumps(element["examples"], ensure_ascii=False),
|
||||||
"hints": json.dumps(element["hints"], ensure_ascii=False)},
|
"hints": json.dumps(element["hints"], ensure_ascii=False)},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1396,6 +1396,8 @@ def _element_fields(data: dict) -> dict | None:
|
|||||||
"description": str(data.get("description", "")).strip(),
|
"description": str(data.get("description", "")).strip(),
|
||||||
"examples": listen["examples"],
|
"examples": listen["examples"],
|
||||||
"hints": listen["hints"],
|
"hints": listen["hints"],
|
||||||
|
"aufgabe": str(data.get("aufgabe", "")).strip(),
|
||||||
|
"loesung": str(data.get("loesung", "")).strip(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1418,7 +1420,7 @@ def _topic_context(topic: str, limit: int = 12000) -> str:
|
|||||||
|
|
||||||
async def generate_element(topic: str, hint: str, provider: str = DEFAULT_PROVIDER) -> dict:
|
async def generate_element(topic: str, hint: str, provider: str = DEFAULT_PROVIDER) -> dict:
|
||||||
"""Erstellt Element-Felder per KI. Fallback: nur Titel aus dem Stichwort."""
|
"""Erstellt Element-Felder per KI. Fallback: nur Titel aus dem Stichwort."""
|
||||||
fallback = {"title": hint.strip() or "Neues Element", "description": "", "examples": [], "hints": []}
|
fallback = {"title": hint.strip() or "Neues Element", "description": "", "examples": [], "hints": [], "aufgabe": "", "loesung": ""}
|
||||||
try:
|
try:
|
||||||
prompt = _prompt(
|
prompt = _prompt(
|
||||||
"Element-Create",
|
"Element-Create",
|
||||||
@@ -1454,7 +1456,7 @@ def _parse_suggestions(stdout: str) -> list[dict] | None:
|
|||||||
text = str(s.get("text", "")).strip()
|
text = str(s.get("text", "")).strip()
|
||||||
target = s.get("target")
|
target = s.get("target")
|
||||||
content = str(s.get("content", "")).strip()
|
content = str(s.get("content", "")).strip()
|
||||||
if text and content and target in ("description", "examples", "hints"):
|
if text and content and target in ("description", "examples", "hints", "aufgabe", "loesung"):
|
||||||
if target == "examples":
|
if target == "examples":
|
||||||
content = _fence(content)
|
content = _fence(content)
|
||||||
suggestions.append({"text": text, "target": target, "content": content})
|
suggestions.append({"text": text, "target": target, "content": content})
|
||||||
@@ -1465,7 +1467,7 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
|
|||||||
"""Zweischrittige Prüfung auf fehlende Infos: Recherche → Verifizieren. None bei Fehler."""
|
"""Zweischrittige Prüfung auf fehlende Infos: Recherche → Verifizieren. None bei Fehler."""
|
||||||
try:
|
try:
|
||||||
element_json = json.dumps(
|
element_json = json.dumps(
|
||||||
{k: element[k] for k in ("title", "description", "examples", "hints")},
|
{k: element[k] for k in ("title", "description", "examples", "hints", "aufgabe", "loesung")},
|
||||||
ensure_ascii=False, indent=1,
|
ensure_ascii=False, indent=1,
|
||||||
)
|
)
|
||||||
context = _topic_context(element["topic"])
|
context = _topic_context(element["topic"])
|
||||||
@@ -1502,7 +1504,7 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
|
|||||||
|
|
||||||
def _element_json(element: dict) -> str:
|
def _element_json(element: dict) -> str:
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{k: element[k] for k in ("title", "description", "examples", "hints")},
|
{k: element[k] for k in ("title", "description", "examples", "hints", "aufgabe", "loesung")},
|
||||||
ensure_ascii=False, indent=1,
|
ensure_ascii=False, indent=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1518,7 +1520,7 @@ def _validate_change(c, element: dict) -> dict | None:
|
|||||||
content = str(c.get("content", "")).strip()
|
content = str(c.get("content", "")).strip()
|
||||||
if not text or action not in ("entfernen", "anpassen", "hinzufuegen"):
|
if not text or action not in ("entfernen", "anpassen", "hinzufuegen"):
|
||||||
return None
|
return None
|
||||||
if target not in ("title", "description", "examples", "hints"):
|
if target not in ("title", "description", "examples", "hints", "aufgabe", "loesung"):
|
||||||
return None
|
return None
|
||||||
if action in ("anpassen", "hinzufuegen") and not content:
|
if action in ("anpassen", "hinzufuegen") and not content:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ FormatType = Literal[
|
|||||||
"FullGuide",
|
"FullGuide",
|
||||||
]
|
]
|
||||||
|
|
||||||
ProviderType = Literal["claude", "minimax", "lokal"]
|
ProviderType = Literal["claude", "minimax", "minimax-direkt", "lokal"]
|
||||||
|
|
||||||
|
|
||||||
class GuideCreateRequest(BaseModel):
|
class GuideCreateRequest(BaseModel):
|
||||||
@@ -86,6 +86,8 @@ class ElementResponse(BaseModel):
|
|||||||
description: str = ""
|
description: str = ""
|
||||||
examples: list[str] = []
|
examples: list[str] = []
|
||||||
hints: list[str] = []
|
hints: list[str] = []
|
||||||
|
aufgabe: str = ""
|
||||||
|
loesung: str = ""
|
||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
|
||||||
@@ -101,6 +103,8 @@ class ElementUpdateRequest(BaseModel):
|
|||||||
description: str | None = None
|
description: str | None = None
|
||||||
examples: list[str] | None = None
|
examples: list[str] | None = None
|
||||||
hints: list[str] | None = None
|
hints: list[str] | None = None
|
||||||
|
aufgabe: str | None = None
|
||||||
|
loesung: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ElementCheckRequest(BaseModel):
|
class ElementCheckRequest(BaseModel):
|
||||||
@@ -109,7 +113,7 @@ class ElementCheckRequest(BaseModel):
|
|||||||
|
|
||||||
class ElementSuggestion(BaseModel):
|
class ElementSuggestion(BaseModel):
|
||||||
text: str
|
text: str
|
||||||
target: Literal["description", "examples", "hints"]
|
target: Literal["description", "examples", "hints", "aufgabe", "loesung"]
|
||||||
content: str
|
content: str
|
||||||
|
|
||||||
|
|
||||||
@@ -120,7 +124,7 @@ class ElementCheckResponse(BaseModel):
|
|||||||
class ElementStyleChange(BaseModel):
|
class ElementStyleChange(BaseModel):
|
||||||
text: str
|
text: str
|
||||||
action: Literal["entfernen", "anpassen", "hinzufuegen"]
|
action: Literal["entfernen", "anpassen", "hinzufuegen"]
|
||||||
target: Literal["title", "description", "examples", "hints"]
|
target: Literal["title", "description", "examples", "hints", "aufgabe", "loesung"]
|
||||||
index: int | None = None
|
index: int | None = None
|
||||||
content: str = ""
|
content: str = ""
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"minimax-direkt": {
|
||||||
|
"npm": "@ai-sdk/anthropic",
|
||||||
|
"name": "MiniMax (ohne Thinking)",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "https://api.minimax.io/anthropic/v1",
|
||||||
|
"apiKey": "{env:MINIMAX_API_KEY}"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"MiniMax-M3": {
|
||||||
|
"name": "MiniMax M3 (ohne Thinking)",
|
||||||
|
"options": {
|
||||||
|
"thinking": { "type": "disabled" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
"name": "Ollama (lokal)",
|
"name": "Ollama (lokal)",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<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>
|
<title>Creator</title>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ function handlePreview(guide) {
|
|||||||
function handleOpenElements() {
|
function handleOpenElements() {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
elementsView.value = true
|
elementsView.value = true
|
||||||
elementsOpen.value = true
|
// Rechte Sidebar bleibt zu — sie öffnet erst beim Klick auf ein Element.
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenElementDetail(el) {
|
function handleOpenElementDetail(el) {
|
||||||
@@ -398,6 +398,11 @@ onUnmounted(() => {
|
|||||||
<div v-else class="empty-main">
|
<div v-else class="empty-main">
|
||||||
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="elementsOpen && selectedTopic"
|
||||||
|
class="elements-backdrop"
|
||||||
|
@click="elementsOpen = false"
|
||||||
|
></div>
|
||||||
<ElementsSidebar
|
<ElementsSidebar
|
||||||
v-if="elementsOpen && selectedTopic"
|
v-if="elementsOpen && selectedTopic"
|
||||||
:topic="selectedTopic"
|
:topic="selectedTopic"
|
||||||
@@ -490,7 +495,7 @@ textarea::placeholder {
|
|||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -500,7 +505,7 @@ textarea::placeholder {
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -518,7 +523,7 @@ textarea::placeholder {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -539,4 +544,20 @@ textarea::placeholder {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 1rem;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ const elements = ref([])
|
|||||||
const query = ref('')
|
const query = ref('')
|
||||||
const creating = ref(false)
|
const creating = ref(false)
|
||||||
const selected = ref(null)
|
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 })
|
watch(() => props.topic, load, { immediate: true })
|
||||||
|
|
||||||
@@ -70,10 +73,10 @@ async function add() {
|
|||||||
|
|
||||||
function select(el) {
|
function select(el) {
|
||||||
selected.value = el
|
selected.value = el
|
||||||
|
tab.value = 'overview'
|
||||||
messages.value = []
|
messages.value = []
|
||||||
input.value = ''
|
input.value = ''
|
||||||
resetCheck()
|
resetCheck()
|
||||||
nextTick(() => inputEl.value?.focus())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
@@ -82,6 +85,48 @@ function back() {
|
|||||||
resetCheck()
|
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) ---
|
// --- KI-Prüfung auf fehlende Infos (Ergebnisse landen als Inline-Vorschläge) ---
|
||||||
const checking = ref(false)
|
const checking = ref(false)
|
||||||
const statusMsg = ref(null)
|
const statusMsg = ref(null)
|
||||||
@@ -205,18 +250,22 @@ async function applyStyleChange(i) {
|
|||||||
const c = styleChanges.value[i]
|
const c = styleChanges.value[i]
|
||||||
applyingStyle.value = true
|
applyingStyle.value = true
|
||||||
try {
|
try {
|
||||||
|
const STRING_TARGETS = ['title', 'description', 'aufgabe', 'loesung']
|
||||||
const fields = {
|
const fields = {
|
||||||
title: selected.value.title,
|
title: selected.value.title,
|
||||||
description: selected.value.description,
|
description: selected.value.description,
|
||||||
examples: [...selected.value.examples],
|
examples: [...selected.value.examples],
|
||||||
hints: [...selected.value.hints],
|
hints: [...selected.value.hints],
|
||||||
|
aufgabe: selected.value.aufgabe || '',
|
||||||
|
loesung: selected.value.loesung || '',
|
||||||
}
|
}
|
||||||
if (c.action === 'entfernen') fields[c.target].splice(c.index, 1)
|
if (c.action === 'entfernen') fields[c.target].splice(c.index, 1)
|
||||||
else if (c.action === 'hinzufuegen') {
|
else if (c.action === 'hinzufuegen') {
|
||||||
if (c.target === 'title') fields.title = c.content
|
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 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
|
else fields[c.target][c.index] = c.content
|
||||||
|
|
||||||
const updated = await updateElement(selected.value.id, fields)
|
const updated = await updateElement(selected.value.id, fields)
|
||||||
@@ -355,7 +404,12 @@ async function send() {
|
|||||||
|
|
||||||
<!-- Detail-Modus -->
|
<!-- Detail-Modus -->
|
||||||
<template v-else>
|
<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>
|
<div v-if="selected.description" class="el-desc markdown" v-html="renderMarkdown(selected.description)"></div>
|
||||||
<ElementSuggestion
|
<ElementSuggestion
|
||||||
v-for="[ci, c] in [...styleAt('title'), ...styleAt('description'), ...styleAdds('description')]"
|
v-for="[ci, c] in [...styleAt('title'), ...styleAt('description'), ...styleAdds('description')]"
|
||||||
@@ -394,13 +448,32 @@ async function send() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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="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="styling" class="check-empty busy-text">Prüft den Stil…</p>
|
||||||
<p v-if="statusMsg && !checking && !styling" class="check-empty">{{ statusMsg }}</p>
|
<p v-if="statusMsg && !checking && !styling" class="check-empty">{{ statusMsg }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="el-chat">
|
<div v-show="tab === 'chat'" class="el-chat">
|
||||||
<div ref="messagesEl" class="chat-messages">
|
<div ref="messagesEl" class="chat-messages">
|
||||||
<p v-if="!messages.length" class="chat-hint">Schreib, was am Element geändert werden soll.</p>
|
<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">
|
<template v-for="(m, i) in messages" :key="i">
|
||||||
@@ -423,6 +496,39 @@ async function send() {
|
|||||||
>{{ loading ? '✕' : '➤' }}</button>
|
>{{ loading ? '✕' : '➤' }}</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</template>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
@@ -431,7 +537,7 @@ async function send() {
|
|||||||
.elements-sidebar {
|
.elements-sidebar {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@@ -441,6 +547,19 @@ async function send() {
|
|||||||
z-index: 30;
|
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 {
|
.el-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -500,6 +619,32 @@ async function send() {
|
|||||||
animation: pulse 1.5s ease-in-out infinite;
|
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 {
|
.el-new {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -641,11 +786,13 @@ async function send() {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-hints-block {
|
.el-hints-block,
|
||||||
|
.el-task-block {
|
||||||
margin-top: 0.9rem;
|
margin-top: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-hints-block h4 {
|
.el-hints-block h4,
|
||||||
|
.el-task-block h4 {
|
||||||
margin: 0 0 0.35rem;
|
margin: 0 0 0.35rem;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -846,13 +993,114 @@ async function send() {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat unten (Muster: TopicDetail-Chat) */
|
/* Chat-Tab (Muster: TopicDetail-Chat) */
|
||||||
.el-chat {
|
.el-chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 0 0 33%;
|
flex: 1;
|
||||||
min-height: 0;
|
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 {
|
.chat-messages {
|
||||||
|
|||||||
@@ -146,11 +146,19 @@ async function scrollToBottom() {
|
|||||||
if (messagesEl.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight
|
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() {
|
async function send() {
|
||||||
const text = input.value.trim()
|
const text = input.value.trim()
|
||||||
if (!text || loading.value || !props.previewGuide) return
|
if (!text || loading.value || !props.previewGuide) return
|
||||||
messages.value.push({ role: 'user', content: text })
|
messages.value.push({ role: 'user', content: text })
|
||||||
input.value = ''
|
input.value = ''
|
||||||
|
nextTick(autoGrow)
|
||||||
loading.value = true
|
loading.value = true
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
try {
|
try {
|
||||||
@@ -237,7 +245,9 @@ async function send() {
|
|||||||
<textarea
|
<textarea
|
||||||
ref="inputEl"
|
ref="inputEl"
|
||||||
v-model="input"
|
v-model="input"
|
||||||
|
rows="3"
|
||||||
placeholder="Frage stellen…"
|
placeholder="Frage stellen…"
|
||||||
|
@input="autoGrow"
|
||||||
@keydown.enter.exact.prevent="send"
|
@keydown.enter.exact.prevent="send"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button :disabled="!input.trim() || loading" @click="send">➤</button>
|
<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
|
/* Flex-Item darf schmaler werden als seine Code-Blöcke — sonst sprengt
|
||||||
deren Mindestbreite auf Mobile das Layout */
|
deren Mindestbreite auf Mobile das Layout */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +279,7 @@ async function send() {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem 2.5rem 5rem;
|
padding: 2rem 2.5rem 5rem;
|
||||||
/* Lese-Zoom nur für den Inhalt — Sidebar/Chat bleiben unverändert */
|
/* Lese-Zoom nur für den Inhalt — Sidebar/Chat bleiben unverändert */
|
||||||
zoom: 1.2;
|
zoom: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
@@ -623,7 +633,7 @@ async function send() {
|
|||||||
bottom: 1.5rem;
|
bottom: 1.5rem;
|
||||||
width: 360px;
|
width: 360px;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
max-height: calc(100vh - 3rem);
|
max-height: calc(100dvh - 3rem);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@@ -706,6 +716,7 @@ async function send() {
|
|||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 0.6rem;
|
padding: 0.6rem;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
@@ -714,12 +725,15 @@ async function send() {
|
|||||||
.chat-input textarea {
|
.chat-input textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
resize: none;
|
resize: none;
|
||||||
height: 38px;
|
min-height: 72px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -729,6 +743,7 @@ async function send() {
|
|||||||
|
|
||||||
.chat-input button {
|
.chat-input button {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
|
flex-shrink: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function providerAvailable(id) {
|
|||||||
return p ? p.available : true
|
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
|
// Tracker oben in der Navigation: Themen gesamt, pro Format erstellt/absolviert
|
||||||
const trackerItems = computed(() => {
|
const trackerItems = computed(() => {
|
||||||
@@ -385,7 +385,7 @@ function confirmDeleteProject(name) {
|
|||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-topic {
|
.new-topic {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ Umfang: SO LANG WIE NÖTIG und SO KURZ WIE MÖGLICH. Markdown: `inline-code` fü
|
|||||||
Jeder Vorschlag:
|
Jeder Vorschlag:
|
||||||
- text: kurz, was geändert wird (max. 12 Wörter, reiner Text)
|
- text: kurz, was geändert wird (max. 12 Wörter, reiner Text)
|
||||||
- action: "entfernen" | "anpassen" | "hinzufuegen"
|
- action: "entfernen" | "anpassen" | "hinzufuegen"
|
||||||
- target: "title" | "description" | "examples" | "hints"
|
- target: "title" | "description" | "examples" | "hints" | "aufgabe" | "loesung"
|
||||||
- index: 0-basierte Position im AKTUELLEN examples- bzw. hints-Array (bei title/description und hinzufuegen: null)
|
- index: 0-basierte Position im AKTUELLEN examples- bzw. hints-Array (bei title/description/aufgabe/loesung und hinzufuegen: null)
|
||||||
- content: der neue vollständige Inhalt (bei entfernen: leer)
|
- content: der neue vollständige Inhalt (bei entfernen: leer)
|
||||||
|
|
||||||
"entfernen" nur für examples/hints. Nur Vorschläge machen, die die Nutzer-Anweisung verlangt.
|
"entfernen" nur für examples/hints. Nur Vorschläge machen, die die Nutzer-Anweisung verlangt.
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ AKTUELLES ELEMENT (JSON):
|
|||||||
KONTEXT (Auszüge aus dem Themen-Material):
|
KONTEXT (Auszüge aus dem Themen-Material):
|
||||||
{context}
|
{context}
|
||||||
|
|
||||||
RECHERCHE — sammle breit alle Kandidaten: fehlende Kernaussagen, wichtige Varianten, typische Stolperfallen, Best Practices. Lieber einen Kandidaten zu viel als einen zu wenig — die Bewertung passiert in einem zweiten Schritt. Nichts vorschlagen, was das Element schon enthält.
|
RECHERCHE — sammle breit alle Kandidaten: fehlende Kernaussagen, wichtige Varianten, typische Stolperfallen, Best Practices. Fehlt eine Übungsaufgabe (aufgabe) oder deren Lösung (loesung), schlage sie vor. Lieber einen Kandidaten zu viel als einen zu wenig — die Bewertung passiert in einem zweiten Schritt. Nichts vorschlagen, was das Element schon enthält.
|
||||||
|
|
||||||
Jeder Kandidat:
|
Jeder Kandidat:
|
||||||
- text: kurze Beschreibung der Lücke (max. 12 Wörter, reiner Text)
|
- text: kurze Beschreibung der Lücke (max. 12 Wörter, reiner Text)
|
||||||
- target: "description" | "examples" | "hints"
|
- target: "description" | "examples" | "hints" | "aufgabe" | "loesung"
|
||||||
- content: fertiger Inhalt zum Einfügen. SO KURZ WIE MÖGLICH, so lang wie nötig. Markdown: `inline-code` für Bezeichner, examples als Codeblock mit Sprachangabe (```sprache), beginnend mit kurzem Kommentar zur Variante (z. B. `<!-- Einzelner Absatz -->`), hints nur wenn WICHTIG oder NÜTZLICH, im Telegrammstil (nur Kernaussage, z. B. "Keine Blockelemente in `<p>`."). Tags/Bezeichner im Fließtext IMMER in Backticks.
|
- content: fertiger Inhalt zum Einfügen. SO KURZ WIE MÖGLICH, so lang wie nötig. Markdown: `inline-code` für Bezeichner, examples als Codeblock mit Sprachangabe (```sprache), beginnend mit kurzem Kommentar zur Variante (z. B. `<!-- Einzelner Absatz -->`), hints nur wenn WICHTIG oder NÜTZLICH, im Telegrammstil (nur Kernaussage, z. B. "Keine Blockelemente in `<p>`."). aufgabe: EINE konkrete, in Minuten lösbare Übungsaufgabe; loesung: die knappe Musterlösung dazu. Tags/Bezeichner im Fließtext IMMER in Backticks.
|
||||||
|
|
||||||
Gib NUR gültiges JSON aus, ohne Code-Fence, ohne weiteren Text:
|
Gib NUR gültiges JSON aus, ohne Code-Fence, ohne weiteren Text:
|
||||||
{{"suggestions": [{{"text": "...", "target": "hints", "content": "..."}}]}}
|
{{"suggestions": [{{"text": "...", "target": "hints", "content": "..."}}]}}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ Erstelle GENAU EIN Element zum Stichwort:
|
|||||||
2. description — was es ist und wozu: MAXIMAL 1–2 Sätze
|
2. description — was es ist und wozu: MAXIMAL 1–2 Sätze
|
||||||
3. examples — GENAU EIN Beispiel: KURZ und SIMPEL, wenige Zeilen Code, das Minimalbeispiel, keine Realwelt-Komplexität. Beginnt mit einem kurzen Kommentar in der Code-Syntax (z. B. `<!-- Einzelner Absatz -->`, `// Mit Default-Wert`), der die Variante benennt.
|
3. examples — GENAU EIN Beispiel: KURZ und SIMPEL, wenige Zeilen Code, das Minimalbeispiel, keine Realwelt-Komplexität. Beginnt mit einem kurzen Kommentar in der Code-Syntax (z. B. `<!-- Einzelner Absatz -->`, `// Mit Default-Wert`), der die Variante benennt.
|
||||||
4. hints — IMMER leere Liste. Hinweise ergänzt der Nutzer später selbst. (Falls je gefordert: TELEGRAMMSTIL, max. 10 Wörter.)
|
4. hints — IMMER leere Liste. Hinweise ergänzt der Nutzer später selbst. (Falls je gefordert: TELEGRAMMSTIL, max. 10 Wörter.)
|
||||||
|
5. aufgabe — GENAU EINE kleine Übungsaufgabe zum Konzept: konkret, in wenigen Minuten lösbar, prüft das Verständnis. Markdown, Code als Codeblock mit Sprachangabe.
|
||||||
|
6. loesung — die Musterlösung zur Aufgabe: knapp, nachvollziehbar, Schritt für Schritt nur wo nötig. Code als Codeblock mit Sprachangabe.
|
||||||
|
|
||||||
Das Element ist ATOMAR: allein verständlich, ohne dass der Leser etwas anderes gelesen hat. Benutzte Begriffe in einem Halbsatz auflösen.
|
Das Element ist ATOMAR: allein verständlich, ohne dass der Leser etwas anderes gelesen hat. Benutzte Begriffe in einem Halbsatz auflösen.
|
||||||
|
|
||||||
@@ -21,4 +23,4 @@ Tonalität: klares Deutsch, direkt, praxisorientiert. Fachbegriffe beim ersten A
|
|||||||
Markdown in description und examples: normale Absätze, `inline-code` für Bezeichner, Codeblöcke mit Sprachangabe (```sprache), **fett** sparsam für Kernaussagen. Keine Überschriften. Code-Beispiele IMMER als Codeblock, nie als Inline-Code. Bezeichner, Tags und Befehle (z. B. `<p>`, `git add`) im Fließtext IMMER in Backticks — nie nackt.
|
Markdown in description und examples: normale Absätze, `inline-code` für Bezeichner, Codeblöcke mit Sprachangabe (```sprache), **fett** sparsam für Kernaussagen. Keine Überschriften. Code-Beispiele IMMER als Codeblock, nie als Inline-Code. Bezeichner, Tags und Befehle (z. B. `<p>`, `git add`) im Fließtext IMMER in Backticks — nie nackt.
|
||||||
|
|
||||||
Gib NUR gültiges JSON aus, ohne Code-Fence, ohne weiteren Text:
|
Gib NUR gültiges JSON aus, ohne Code-Fence, ohne weiteren Text:
|
||||||
{{"title": "...", "description": "...", "examples": ["```sprache\n...\n```"], "hints": []}}
|
{{"title": "...", "description": "...", "examples": ["```sprache\n...\n```"], "hints": [], "aufgabe": "...", "loesung": "..."}}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ STIL-REGELN:
|
|||||||
2. description — was es ist und wozu: MAXIMAL 1–2 Sätze
|
2. description — was es ist und wozu: MAXIMAL 1–2 Sätze
|
||||||
3. examples — KURZ und SIMPEL: wenige Zeilen Code, Minimalbeispiel, keine Realwelt-Komplexität. Ein Beispiel pro relevanter Variante, geordnet vom Üblichen zum Speziellen. Als Codeblock mit Sprachangabe (```sprache), nie als Inline-Code. Jedes Beispiel beginnt mit einem kurzen Kommentar in der Code-Syntax (z. B. `<!-- Einzelner Absatz -->`), der die Variante benennt.
|
3. examples — KURZ und SIMPEL: wenige Zeilen Code, Minimalbeispiel, keine Realwelt-Komplexität. Ein Beispiel pro relevanter Variante, geordnet vom Üblichen zum Speziellen. Als Codeblock mit Sprachangabe (```sprache), nie als Inline-Code. Jedes Beispiel beginnt mit einem kurzen Kommentar in der Code-Syntax (z. B. `<!-- Einzelner Absatz -->`), der die Variante benennt.
|
||||||
4. hints — jeder Hinweis muss WICHTIG oder NÜTZLICH sein: Stolperfalle, Merksatz oder Best Practice mit echtem Praxiswert. Selbstverständliches, Nischenwissen und Redundantes zum Element entfernen. Telegrammstil: nur die Kernaussage, Füllverben und Herleitungen streichen.
|
4. hints — jeder Hinweis muss WICHTIG oder NÜTZLICH sein: Stolperfalle, Merksatz oder Best Practice mit echtem Praxiswert. Selbstverständliches, Nischenwissen und Redundantes zum Element entfernen. Telegrammstil: nur die Kernaussage, Füllverben und Herleitungen streichen.
|
||||||
|
aufgabe — EINE konkrete, in Minuten lösbare Übungsaufgabe, die das Verständnis prüft. loesung — knappe, nachvollziehbare Musterlösung dazu. Beide: Code als Codeblock mit Sprachangabe.
|
||||||
Vorher: "Browser fügen standardmäßig vertikalen Abstand vor und nach `<p>` ein — anpassbar mit `margin`."
|
Vorher: "Browser fügen standardmäßig vertikalen Abstand vor und nach `<p>` ein — anpassbar mit `margin`."
|
||||||
Nachher: "Browser-Abstand um `<p>` per `margin` anpassbar."
|
Nachher: "Browser-Abstand um `<p>` per `margin` anpassbar."
|
||||||
5. Umfang: SO LANG WIE NÖTIG und SO KURZ WIE MÖGLICH. Jedes Wort muss seinen Platz verdienen — Füllwörter, Nebensätze ohne Informationswert und Selbstverständliches streichen. Aber: Kürze nie auf Kosten der Verständlichkeit oder Korrektheit.
|
5. Umfang: SO LANG WIE NÖTIG und SO KURZ WIE MÖGLICH. Jedes Wort muss seinen Platz verdienen — Füllwörter, Nebensätze ohne Informationswert und Selbstverständliches streichen. Aber: Kürze nie auf Kosten der Verständlichkeit oder Korrektheit.
|
||||||
@@ -17,8 +18,8 @@ STIL-REGELN:
|
|||||||
Schlage für jeden Stil-Verstoß GENAU EINE Änderung vor:
|
Schlage für jeden Stil-Verstoß GENAU EINE Änderung vor:
|
||||||
- text: kurz, was und warum (max. 12 Wörter, reiner Text)
|
- text: kurz, was und warum (max. 12 Wörter, reiner Text)
|
||||||
- action: "entfernen" | "anpassen" | "hinzufuegen"
|
- action: "entfernen" | "anpassen" | "hinzufuegen"
|
||||||
- target: "title" | "description" | "examples" | "hints"
|
- target: "title" | "description" | "examples" | "hints" | "aufgabe" | "loesung"
|
||||||
- index: 0-basierte Position im AKTUELLEN examples- bzw. hints-Array (bei title/description: null; bei hinzufuegen: null)
|
- index: 0-basierte Position im AKTUELLEN examples- bzw. hints-Array (bei title/description/aufgabe/loesung: null; bei hinzufuegen: null)
|
||||||
- content: der neue/vollständige Inhalt (bei entfernen: leer)
|
- content: der neue/vollständige Inhalt (bei entfernen: leer)
|
||||||
|
|
||||||
"entfernen" nur für examples/hints. "hinzufuegen" sparsam — nur wenn eine Stil-Regel es verlangt (z. B. fehlender Varianten-Kommentar gehört zu "anpassen", nicht "hinzufuegen"). Erfüllt etwas die Regeln schon: NICHT anfassen.
|
"entfernen" nur für examples/hints. "hinzufuegen" sparsam — nur wenn eine Stil-Regel es verlangt (z. B. fehlender Varianten-Kommentar gehört zu "anpassen", nicht "hinzufuegen"). Erfüllt etwas die Regeln schon: NICHT anfassen.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Prüfe JEDEN Kandidaten kritisch:
|
|||||||
1. WICHTIG? Muss ein Lerner das wissen? Nice-to-haves und Nischenwissen ablehnen.
|
1. WICHTIG? Muss ein Lerner das wissen? Nice-to-haves und Nischenwissen ablehnen.
|
||||||
2. REDUNDANT? Steckt die Info schon im Element oder in einem anderen Kandidaten? Ablehnen bzw. Duplikate zusammenführen.
|
2. REDUNDANT? Steckt die Info schon im Element oder in einem anderen Kandidaten? Ablehnen bzw. Duplikate zusammenführen.
|
||||||
3. KORREKT? Fachlich falsch oder irreführend → ablehnen.
|
3. KORREKT? Fachlich falsch oder irreführend → ablehnen.
|
||||||
4. PASST das target ("description" | "examples" | "hints")? Sonst korrigieren.
|
4. PASST das target ("description" | "examples" | "hints" | "aufgabe" | "loesung")? Sonst korrigieren. Höchstens EIN Vorschlag je für "aufgabe" und "loesung".
|
||||||
|
|
||||||
Behalte nur Kandidaten, die alle Prüfungen bestehen. Verbessere dabei content auf die Stil-Regeln: SO LANG WIE NÖTIG und SO KURZ WIE MÖGLICH; `inline-code` für Bezeichner; examples als Codeblock mit Sprachangabe und kurzem Varianten-Kommentar; hints im Telegrammstil (nur Kernaussage, Kürze nie auf Kosten der Verständlichkeit).
|
Behalte nur Kandidaten, die alle Prüfungen bestehen. Verbessere dabei content auf die Stil-Regeln: SO LANG WIE NÖTIG und SO KURZ WIE MÖGLICH; `inline-code` für Bezeichner; examples als Codeblock mit Sprachangabe und kurzem Varianten-Kommentar; hints im Telegrammstil (nur Kernaussage, Kürze nie auf Kosten der Verständlichkeit).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user