Commit c9c6a73e authored by Kantz's avatar Kantz
Browse files

Quellen wieder zugreifbar

parent 8a194678
from __future__ import annotations from __future__ import annotations
from typing import Dict, Iterable, Tuple from typing import Dict, Iterable, Protocol, Set, Tuple
import re import re
from urllib.parse import quote from urllib.parse import quote
from app.deterministic_services import Source, SourceID
_BRACKET_RE = re.compile(r"\[(?P<ref>[^\[\]]+)\]") _BRACKET_RE = re.compile(r"\[(?P<ref>[^\[\]]+)\]")
def _source_id_key(source_id: SourceID) -> str: class _SourceIDLike(Protocol):
chapter_title: str | None
section_title: str | None
subsection_title: str | None
title: str | None
doc_type: str | None
def to_string(self) -> str: ...
class _SourceLike(Protocol):
source_id: _SourceIDLike
def _source_id_key(source_id: _SourceIDLike) -> str:
parts = [ parts = [
source_id.chapter_title or "", source_id.chapter_title or "",
source_id.section_title or "", source_id.section_title or "",
...@@ -30,9 +42,11 @@ def _normalize_ref_token(token: str) -> str: ...@@ -30,9 +42,11 @@ def _normalize_ref_token(token: str) -> str:
return t.casefold() return t.casefold()
def _build_source_index(sources: Iterable[Source]) -> Dict[str, str]: def _build_source_index(sources: Iterable[_SourceLike]) -> Tuple[Dict[str, str], Set[str]]:
index: Dict[str, str] = {} index: Dict[str, str] = {}
doc_types: Set[str] = set()
for source in sources: for source in sources:
doc_types.add(_normalize_ref_token(source.source_id.doc_type or ""))
key = _source_id_key(source.source_id) key = _source_id_key(source.source_id)
normalized_key = _normalize_ref_token(key) normalized_key = _normalize_ref_token(key)
index.setdefault(normalized_key, key) index.setdefault(normalized_key, key)
...@@ -46,22 +60,46 @@ def _build_source_index(sources: Iterable[Source]) -> Dict[str, str]: ...@@ -46,22 +60,46 @@ def _build_source_index(sources: Iterable[Source]) -> Dict[str, str]:
source.source_id.subsection_title, source.source_id.subsection_title,
source.source_id.title, source.source_id.title,
] ]
filtered = [p for p in parts if p and p.lower() != "none"] for start in range(len(parts)):
if filtered: filtered = [
minimal = "|".join(filtered + [source.source_id.doc_type]) p for p in parts[start:]
index.setdefault(_normalize_ref_token(minimal), key) if p and p.lower() != "none"
]
if filtered:
minimal = "|".join(filtered + [source.source_id.doc_type])
index.setdefault(_normalize_ref_token(minimal), key)
if source.source_id.title: if source.source_id.title:
compact = f"{source.source_id.title}|{source.source_id.doc_type}" compact = f"{source.source_id.title}|{source.source_id.doc_type}"
index.setdefault(_normalize_ref_token(compact), key) index.setdefault(_normalize_ref_token(compact), key)
return index return index, {dtype for dtype in doc_types if dtype}
def _candidate_tokens(token: str, doc_types: Set[str]) -> Iterable[str]:
normalized = _normalize_ref_token(token)
yield normalized
parts = [part for part in normalized.split("|") if part]
if not parts:
return
for idx, part in enumerate(parts):
if part in doc_types:
yield "|".join(parts[: idx + 1])
if len(parts) >= 2:
yield "|".join(parts[-2:])
if len(parts) >= 3:
yield "|".join(parts[-3:])
if len(parts) >= 4:
yield "|".join(parts[-4:])
def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]: def decode_references(text: str, sources: Iterable[_SourceLike]) -> Tuple[str, int]:
if not text: if not text:
return text, 0 return text, 0
index = _build_source_index(sources) index, doc_types = _build_source_index(sources)
if not index: if not index:
return text, 0 return text, 0
...@@ -75,8 +113,11 @@ def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]: ...@@ -75,8 +113,11 @@ def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]:
if end < len(text) and text[end:end + 1] == "(": if end < len(text) and text[end:end + 1] == "(":
return match.group(0) return match.group(0)
normalized = _normalize_ref_token(ref) key = None
key = index.get(normalized) for candidate in _candidate_tokens(ref, doc_types):
key = index.get(candidate)
if key:
break
if not key: if not key:
return match.group(0) return match.group(0)
......
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import importlib.util
import unittest
_MODULE_PATH = (
Path(__file__).resolve().parents[1]
/ "app"
/ "deterministic_services"
/ "referenz_decoder.py"
)
_SPEC = importlib.util.spec_from_file_location("referenz_decoder", _MODULE_PATH)
assert _SPEC is not None and _SPEC.loader is not None
_MODULE = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(_MODULE)
decode_references = _MODULE.decode_references
@dataclass
class SourceIDStub:
chapter_title: str | None
section_title: str | None
subsection_title: str | None
title: str
doc_type: str
def to_string(self) -> str:
parts = [self.title]
if self.subsection_title:
parts.insert(0, self.subsection_title)
if self.section_title:
parts.insert(0, self.section_title)
if self.chapter_title:
parts.insert(0, self.chapter_title)
return f"[{'|'.join(parts)}|{self.doc_type}]"
@dataclass
class SourceStub:
source_id: SourceIDStub
def _mk_source() -> SourceStub:
return SourceStub(
source_id=SourceIDStub(
chapter_title="Kapitel 1",
section_title="Lineare Funktionen",
subsection_title="Steigung",
title="Definition",
doc_type="subsection",
)
)
class ReferenzDecoderUnitTest(unittest.TestCase):
def test_decodes_reference_without_chapter(self) -> None:
source = _mk_source()
text = "Siehe [Lineare Funktionen|Steigung|Definition|subsection]."
decoded, count = decode_references(text, [source])
self.assertEqual(count, 1)
self.assertIn(
"(doc://Kapitel%201%7CLineare%20Funktionen%7CSteigung%7CDefinition%7Csubsection)",
decoded,
)
def test_decodes_reference_with_extra_tail_segment(self) -> None:
source = _mk_source()
text = "Siehe [Lineare Funktionen/Steigung/Definition | subsection | ...]."
decoded, count = decode_references(text, [source])
self.assertEqual(count, 1)
self.assertIn(
"(doc://Kapitel%201%7CLineare%20Funktionen%7CSteigung%7CDefinition%7Csubsection)",
decoded,
)
def test_skips_existing_markdown_links(self) -> None:
source = _mk_source()
text = "Bereits verlinkt: [Definition](doc://foo)."
decoded, count = decode_references(text, [source])
self.assertEqual(count, 0)
self.assertEqual(decoded, text)
if __name__ == "__main__":
unittest.main()
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
import type { RetrievedDoc } from "../Retrieval/DocPanel"; import type { RetrievedDoc } from "../Retrieval/DocPanel";
import { t } from "../../i18n"; import { t } from "../../i18n";
...@@ -54,6 +54,12 @@ export default function MessageBubble({ ...@@ -54,6 +54,12 @@ export default function MessageBubble({
</div> </div>
<div className="message-text"> <div className="message-text">
<ReactMarkdown <ReactMarkdown
urlTransform={(url) => {
if (url.startsWith("doc://")) {
return url;
}
return defaultUrlTransform(url);
}}
components={{ components={{
a: ({ href, children, ...props }) => { a: ({ href, children, ...props }) => {
const target = href || ""; const target = href || "";
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment