mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-20 11:45:24 -04:00
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and small visual nudges across the app. Highlights: Settings restructure - Add Models: split into separate Local + API cards (no more in-card tabs); each fuses Type/Provider with the URL input. - Added Models: new dedicated sidebar tab, with Probe + Clear-offline pulled into its header; Local/API sub-section icons accent-tinted. - Search: Web Search and a new Deep Research card (Model + tuning), with a cross-link to AI Defaults. Provider hints use real clickable anchors; Web Search Test button shows a whirlpool spinner. - AI Defaults: Image Generation card returns; Research Model card carries only Endpoint+Model with a cross-link to Search; Vision / Default / Utility fallbacks unified under one numbered-row design matching Search's chain. - API Permissions (was 'API Tokens'): per-row rename, inline Permissions toggle that expands the scope-edit panel, in-field copy icons (icon→check on success). Empty state accent-tinted. - Integrations: + Add Integration drops a type-picker menu directly under the button (drop-up on tight viewports); each integration form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses the same accent-outlined Save/Test/Cancel buttons right-aligned. - Danger Zone: Wipe→Delete with trash icons; new 'Delete everything' row at the bottom that loops every category. AI Synthesis (Reminders) - Persona dropdown sourced from PROMPT_TEMPLATES + custom preset. - src/reminder_personas.py mirrors the five built-ins for the server-side synthesis path. - dispatch_reminder() reads reminder_llm_persona and uses the persona's system prompt; empty/unknown falls back to warm-neutral. Esc handling - Kebab menus and the provider picker intercept Esc in capture phase so dismissing a popup no longer closes the whole Settings modal. Accent tinting - Scoped CSS rule across data-settings-panel=ai/services/added-models/ search/integrations/reminders for card h2 icons + the Added Models sub-section icons. Codex/Claude integration form - No more auto-creation on form open — explicit Create token button. - New tokens start with every scope granted; existing tokens move out of the integration form into the API Permissions card. - Setup reveal: copy buttons inline inside the token + setup code blocks; shorter subtitle wording. Misc visual polish - Save/Test/Cancel uniformly accent-outlined and right-aligned on every integration form. - Provider logos render inline next to the search fallback selects and the Deep Research Search dropdown. - Trash icons in fallback rows bumped to 20x20 so they fill the 32px button. - Image generation default flipped to off.
This commit is contained in:
@@ -40,15 +40,59 @@ def load_markitdown():
|
||||
return MarkItDown
|
||||
|
||||
|
||||
def _extract_docx_native(path: str) -> str | None:
|
||||
"""Pure-Python .docx text extractor — no external deps.
|
||||
|
||||
A .docx file is just a zip of XML. The body prose lives in <w:t> runs
|
||||
inside <w:p> paragraphs. Iterating with ElementTree (rather than
|
||||
re.findall) keeps paragraph breaks intact and lets the XML parser handle
|
||||
namespaces + entity unescaping. Loses tables, footnotes, images and
|
||||
list bullets — keeps ~95% of "summarize this doc" content, which is the
|
||||
case people hit when markitdown isn't installed.
|
||||
"""
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
ns = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
|
||||
try:
|
||||
with zipfile.ZipFile(path) as z:
|
||||
xml_bytes = z.read("word/document.xml")
|
||||
except (zipfile.BadZipFile, KeyError, OSError):
|
||||
return None
|
||||
try:
|
||||
root = ET.fromstring(xml_bytes)
|
||||
except ET.ParseError:
|
||||
return None
|
||||
paragraphs: list[str] = []
|
||||
for para in root.iter(f"{ns}p"):
|
||||
runs = [t.text or "" for t in para.iter(f"{ns}t")]
|
||||
line = "".join(runs).strip()
|
||||
if line:
|
||||
paragraphs.append(line)
|
||||
return "\n\n".join(paragraphs) if paragraphs else None
|
||||
|
||||
|
||||
def convert_to_markdown(path: str) -> str | None:
|
||||
"""Convert a document to Markdown text via markitdown.
|
||||
|
||||
Returns the extracted Markdown, or ``None`` if markitdown is unavailable or
|
||||
the conversion fails — callers degrade gracefully rather than erroring.
|
||||
|
||||
Fallback: when markitdown isn't installed and the file is a .docx, run
|
||||
the bundled pure-Python extractor so the most common case (Word docs)
|
||||
works out of the box. Other Office/EPUB formats still need markitdown.
|
||||
"""
|
||||
try:
|
||||
markitdown_cls = load_markitdown()
|
||||
except RuntimeError:
|
||||
if isinstance(path, str) and path.lower().endswith(".docx"):
|
||||
text = _extract_docx_native(path)
|
||||
if text:
|
||||
logger.info(
|
||||
"markitdown not installed — used native .docx extractor for %s",
|
||||
path,
|
||||
)
|
||||
return text
|
||||
logger.warning("markitdown not installed; cannot extract %s", path)
|
||||
return None
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user