diff --git a/backend/generator.py b/backend/generator.py index 38fe692..42fb82a 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -550,6 +550,36 @@ Kein weiterer Text, nur das JSON-Array. """ +def _build_topic_suggest_prompt(problem: str, existing_topics: list[str]) -> str: + template = (TEMPLATES_DIR / "Format" / "Suche.md").read_text(encoding="utf-8") + existing = "\n".join(f"- {t}" for t in existing_topics) if existing_topics else "(keine)" + return template.replace("{problem}", problem).replace("{existing}", existing) + + +async def suggest_topics(problem: str, existing_topics: list[str] | None = None) -> list[dict]: + try: + prompt = _build_topic_suggest_prompt(problem, existing_topics or []) + returncode, stdout, stderr = await _run_claude( + "topic-suggest-" + str(uuid.uuid4()), prompt, 120, tools=None, model=MODEL_BAUSTEIN_GEN + ) + if returncode != 0: + return [] + items = _parse_json(stdout) + if not isinstance(items, list): + return [] + result = [] + for item in items: + if not isinstance(item, dict): + continue + title = str(item.get("title", "")).strip()[:100] + if not title: + continue + result.append({"title": title, "reason": str(item.get("reason", "")).strip()}) + return result + except Exception: + return [] + + async def sort_bausteine(topic: str, bausteine: list[dict], instructions: str = "") -> None: _sorting.add(topic) try: diff --git a/backend/models.py b/backend/models.py index 33c441b..c7ebe03 100644 --- a/backend/models.py +++ b/backend/models.py @@ -68,3 +68,12 @@ class SuggestionResponse(BaseModel): example: str status: str created_at: str + + +class TopicSuggestRequest(BaseModel): + problem: str = Field(min_length=1, max_length=2000) + + +class TopicSuggestion(BaseModel): + title: str + reason: str diff --git a/backend/routes.py b/backend/routes.py index 2c120b6..96c61fb 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -11,10 +11,11 @@ from database import ( create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein, list_suggestions, get_suggestion, update_suggestion, delete_suggestion, ) -from generator import generate_guide, rework_guide, cancel_guide, generate_suggestions, generate_baustein_detail, rework_baustein, sort_bausteine, is_suggestions_generating, is_sorting +from generator import generate_guide, rework_guide, cancel_guide, generate_suggestions, generate_baustein_detail, rework_baustein, sort_bausteine, suggest_topics, is_suggestions_generating, is_sorting from models import ( GuideCreateRequest, GuideReworkRequest, GuideResponse, BausteinCreateRequest, BausteinReworkRequest, BausteinSortRequest, BausteinResponse, SuggestionResponse, + TopicSuggestRequest, TopicSuggestion, ) from paths import final_paths @@ -26,6 +27,13 @@ async def get_formats(): return FORMAT_META +@router.post("/topic-suggestions", response_model=list[TopicSuggestion]) +async def topic_suggestions(req: TopicSuggestRequest): + guides = await list_guides() + existing_topics = sorted({g["topic"] for g in guides}) + return await suggest_topics(req.problem.strip(), existing_topics) + + @router.post("/guides", response_model=GuideResponse) async def create(req: GuideCreateRequest): now = datetime.now(timezone.utc).isoformat() diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5ac44b4..6621d55 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,12 +4,14 @@ import { fetchGuides, createGuide as apiCreate, deleteGuide, cancelGuide as apiC import TopicSidebar from './components/TopicSidebar.vue' import TopicDetail from './components/TopicDetail.vue' import BausteineView from './components/BausteineView.vue' +import HelpChat from './components/HelpChat.vue' const guides = ref([]) const manualTopics = ref([]) const selectedTopic = ref(null) const previewGuide = ref(null) const showBausteine = ref(false) +const showHelp = ref(false) const bausteineRefreshKey = ref(0) const sidebarPinned = ref(localStorage.getItem('sidebarPinned') !== 'false') const sidebarSticky = ref(false) @@ -94,6 +96,7 @@ function selectTopic(topic) { selectedTopic.value = topic previewGuide.value = null showBausteine.value = false + showHelp.value = false nextTick(autoPreview) } @@ -103,6 +106,11 @@ function createTopic(topic) { } selectedTopic.value = topic previewGuide.value = null + showHelp.value = false +} + +function onHelpSelect(title) { + createTopic(title) } async function handleFormatClick({ format, instructions }) { @@ -221,9 +229,15 @@ onUnmounted(() => { @addBaustein="handleSidebarAddBaustein" @togglePin="toggleSidebarPin" @sidebarLeave="onSidebarLeave" + @openHelp="showHelp = true" + /> + diff --git a/frontend/src/api.js b/frontend/src/api.js index 1f69021..a811a02 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -44,6 +44,15 @@ export function htmlUrl(id) { return `${BASE}/guides/${id}/html` } +export async function suggestTopics(problem) { + const res = await fetch(`${BASE}/topic-suggestions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ problem }), + }) + return res.json() +} + export async function fetchBausteine(topic) { const res = await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`) return res.json() diff --git a/frontend/src/components/HelpChat.vue b/frontend/src/components/HelpChat.vue new file mode 100644 index 0000000..adcb223 --- /dev/null +++ b/frontend/src/components/HelpChat.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/frontend/src/components/TopicSidebar.vue b/frontend/src/components/TopicSidebar.vue index f50a219..1be688f 100644 --- a/frontend/src/components/TopicSidebar.vue +++ b/frontend/src/components/TopicSidebar.vue @@ -11,7 +11,7 @@ const props = defineProps({ pinned: { type: Boolean, default: true }, }) -const emit = defineEmits(['select', 'create', 'formatClick', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'preview', 'rework', 'showBausteine', 'addBaustein', 'togglePin', 'sidebarLeave']) +const emit = defineEmits(['select', 'create', 'formatClick', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'preview', 'rework', 'showBausteine', 'addBaustein', 'togglePin', 'sidebarLeave', 'openHelp']) const quickBausteinTitle = ref('') @@ -140,6 +140,11 @@ function confirmDeleteTopic(topic) { :title="pinned ? 'Sidebar ausblenden' : 'Sidebar fixieren'" @click="emit('togglePin')" >{{ pinned ? '⇤' : '⇥' }} +