feat: initial LibreOffice Writer Ollama sidebar extension scaffold
This commit is contained in:
235
ollama_sidebar.py
Normal file
235
ollama_sidebar.py
Normal file
@@ -0,0 +1,235 @@
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
import uno
|
||||
import unohelper
|
||||
from com.sun.star.task import XJobExecutor
|
||||
from com.sun.star.awt import XActionListener
|
||||
|
||||
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".ollama_writer_sidebar")
|
||||
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
||||
|
||||
DEFAULT_CFG = {
|
||||
"ollama_url": "http://127.0.0.1:11434",
|
||||
"model": "llama3.1:8b",
|
||||
"prompt": "Überarbeite den Text klarer, Sinn beibehalten."
|
||||
}
|
||||
|
||||
|
||||
def load_cfg():
|
||||
try:
|
||||
os.makedirs(CONFIG_DIR, exist_ok=True)
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
save_cfg(DEFAULT_CFG)
|
||||
return dict(DEFAULT_CFG)
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
for k, v in DEFAULT_CFG.items():
|
||||
data.setdefault(k, v)
|
||||
return data
|
||||
except Exception:
|
||||
return dict(DEFAULT_CFG)
|
||||
|
||||
|
||||
def save_cfg(cfg):
|
||||
os.makedirs(CONFIG_DIR, exist_ok=True)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def ollama_generate(url, model, prompt, source):
|
||||
full_prompt = f"{prompt}\n\nTEXT:\n{source}"
|
||||
payload = json.dumps({"model": model, "prompt": full_prompt, "stream": False}).encode("utf-8")
|
||||
req = urllib.request.Request(url.rstrip("/") + "/api/generate", data=payload, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
return data.get("response", "")
|
||||
|
||||
|
||||
def get_doc_text(doc):
|
||||
return doc.Text.String if hasattr(doc, "Text") else ""
|
||||
|
||||
|
||||
def get_selected_text(doc):
|
||||
try:
|
||||
sel = doc.getCurrentSelection()
|
||||
if sel and sel.getCount() > 0:
|
||||
part = sel.getByIndex(0)
|
||||
return getattr(part, "String", "")
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def replace_selection_or_append(doc, txt):
|
||||
try:
|
||||
sel = doc.getCurrentSelection()
|
||||
if sel and sel.getCount() > 0:
|
||||
part = sel.getByIndex(0)
|
||||
if hasattr(part, "String"):
|
||||
part.String = txt
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
doc.Text.insertString(doc.Text.End, "\n" + txt, False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class _BtnListener(unohelper.Base, XActionListener):
|
||||
def __init__(self, owner):
|
||||
self.owner = owner
|
||||
|
||||
def actionPerformed(self, event):
|
||||
cmd = event.ActionCommand
|
||||
self.owner.on_action(cmd)
|
||||
|
||||
def disposing(self, event):
|
||||
pass
|
||||
|
||||
|
||||
class OllamaDialog:
|
||||
def __init__(self, ctx):
|
||||
self.ctx = ctx
|
||||
self.smgr = ctx.getServiceManager()
|
||||
self.desktop = self.smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
|
||||
self.doc = self.desktop.getCurrentComponent()
|
||||
self.cfg = load_cfg()
|
||||
|
||||
self.dialog = None
|
||||
self.controls = {}
|
||||
|
||||
def _add_control(self, model, name, ctype, x, y, w, h, props):
|
||||
c = model.createInstance(ctype)
|
||||
c.Name = name
|
||||
c.PositionX = x
|
||||
c.PositionY = y
|
||||
c.Width = w
|
||||
c.Height = h
|
||||
for k, v in props.items():
|
||||
setattr(c, k, v)
|
||||
model.insertByName(name, c)
|
||||
|
||||
def create(self):
|
||||
dm = self.smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialogModel", self.ctx)
|
||||
dm.PositionX = 120
|
||||
dm.PositionY = 80
|
||||
dm.Width = 300
|
||||
dm.Height = 230
|
||||
dm.Title = "Ollama Rewrite (Sidebar-MVP)"
|
||||
|
||||
self._add_control(dm, "lbl1", "com.sun.star.awt.UnoControlFixedTextModel", 6, 6, 40, 10, {"Label": "URL"})
|
||||
self._add_control(dm, "url", "com.sun.star.awt.UnoControlEditModel", 48, 4, 245, 12, {"Text": self.cfg.get("ollama_url", "")})
|
||||
|
||||
self._add_control(dm, "lbl2", "com.sun.star.awt.UnoControlFixedTextModel", 6, 20, 40, 10, {"Label": "Model"})
|
||||
self._add_control(dm, "model", "com.sun.star.awt.UnoControlEditModel", 48, 18, 245, 12, {"Text": self.cfg.get("model", "")})
|
||||
|
||||
self._add_control(dm, "lbl3", "com.sun.star.awt.UnoControlFixedTextModel", 6, 34, 40, 10, {"Label": "Prompt"})
|
||||
self._add_control(dm, "prompt", "com.sun.star.awt.UnoControlEditModel", 48, 32, 245, 28, {
|
||||
"MultiLine": True,
|
||||
"VScroll": True,
|
||||
"Text": self.cfg.get("prompt", "")
|
||||
})
|
||||
|
||||
src = get_selected_text(self.doc) or get_doc_text(self.doc)
|
||||
self._add_control(dm, "lbl4", "com.sun.star.awt.UnoControlFixedTextModel", 6, 62, 40, 10, {"Label": "Quelle"})
|
||||
self._add_control(dm, "source", "com.sun.star.awt.UnoControlEditModel", 48, 60, 245, 58, {
|
||||
"MultiLine": True,
|
||||
"VScroll": True,
|
||||
"Text": src
|
||||
})
|
||||
|
||||
self._add_control(dm, "lbl5", "com.sun.star.awt.UnoControlFixedTextModel", 6, 122, 40, 10, {"Label": "Ziel"})
|
||||
self._add_control(dm, "target", "com.sun.star.awt.UnoControlEditModel", 48, 120, 245, 58, {
|
||||
"MultiLine": True,
|
||||
"VScroll": True,
|
||||
"Text": ""
|
||||
})
|
||||
|
||||
self._add_control(dm, "rewrite", "com.sun.star.awt.UnoControlButtonModel", 48, 184, 58, 14, {"Label": "Bearbeiten", "PushButtonType": 0})
|
||||
self._add_control(dm, "copy", "com.sun.star.awt.UnoControlButtonModel", 110, 184, 58, 14, {"Label": "Clipboard", "PushButtonType": 0})
|
||||
self._add_control(dm, "insert", "com.sun.star.awt.UnoControlButtonModel", 172, 184, 58, 14, {"Label": "Einfügen", "PushButtonType": 0})
|
||||
self._add_control(dm, "close", "com.sun.star.awt.UnoControlButtonModel", 234, 184, 58, 14, {"Label": "Schließen", "PushButtonType": 0})
|
||||
|
||||
self._add_control(dm, "status", "com.sun.star.awt.UnoControlFixedTextModel", 48, 202, 245, 12, {"Label": "Bereit"})
|
||||
|
||||
self.dialog = self.smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", self.ctx)
|
||||
self.dialog.setModel(dm)
|
||||
|
||||
toolkit = self.smgr.createInstanceWithContext("com.sun.star.awt.ExtToolkit", self.ctx)
|
||||
self.dialog.createPeer(toolkit, None)
|
||||
|
||||
listener = _BtnListener(self)
|
||||
for btn, cmd in [("rewrite", "rewrite"), ("copy", "copy"), ("insert", "insert"), ("close", "close")]:
|
||||
c = self.dialog.getControl(btn)
|
||||
c.setActionCommand(cmd)
|
||||
c.addActionListener(listener)
|
||||
|
||||
self.listener = listener
|
||||
|
||||
def set_status(self, txt):
|
||||
self.dialog.getControl("status").getModel().Label = txt
|
||||
|
||||
def on_action(self, cmd):
|
||||
if cmd == "close":
|
||||
self.dialog.endExecute()
|
||||
return
|
||||
|
||||
if cmd == "rewrite":
|
||||
try:
|
||||
url = self.dialog.getControl("url").getModel().Text.strip()
|
||||
model = self.dialog.getControl("model").getModel().Text.strip()
|
||||
prompt = self.dialog.getControl("prompt").getModel().Text.strip()
|
||||
source = self.dialog.getControl("source").getModel().Text
|
||||
|
||||
self.cfg.update({"ollama_url": url, "model": model, "prompt": prompt})
|
||||
save_cfg(self.cfg)
|
||||
|
||||
self.set_status("Bearbeite…")
|
||||
out = ollama_generate(url, model, prompt, source)
|
||||
self.dialog.getControl("target").getModel().Text = out
|
||||
self.set_status("Fertig")
|
||||
except Exception as e:
|
||||
self.set_status(f"Fehler: {e}")
|
||||
return
|
||||
|
||||
if cmd == "copy":
|
||||
txt = self.dialog.getControl("target").getModel().Text
|
||||
try:
|
||||
service = self.smgr.createInstanceWithContext("com.sun.star.datatransfer.clipboard.SystemClipboard", self.ctx)
|
||||
transferable = self.smgr.createInstanceWithContext("com.sun.star.datatransfer.DataFlavor", self.ctx)
|
||||
_ = service, transferable
|
||||
except Exception:
|
||||
pass
|
||||
# Simpler fallback: keep in target and close
|
||||
self.dialog.endExecute()
|
||||
return
|
||||
|
||||
if cmd == "insert":
|
||||
txt = self.dialog.getControl("target").getModel().Text
|
||||
replace_selection_or_append(self.doc, txt)
|
||||
self.dialog.endExecute()
|
||||
return
|
||||
|
||||
def run(self):
|
||||
self.create()
|
||||
self.dialog.execute()
|
||||
|
||||
|
||||
class Toggle(unohelper.Base, XJobExecutor):
|
||||
def __init__(self, ctx):
|
||||
self.ctx = ctx
|
||||
|
||||
def trigger(self, args):
|
||||
dlg = OllamaDialog(self.ctx)
|
||||
dlg.run()
|
||||
|
||||
|
||||
g_ImplementationHelper = unohelper.ImplementationHelper()
|
||||
g_ImplementationHelper.addImplementation(
|
||||
Toggle,
|
||||
"de.aquantico.ollama.sidebar.Toggle",
|
||||
("com.sun.star.task.Job", "com.sun.star.task.XJobExecutor",)
|
||||
)
|
||||
Reference in New Issue
Block a user