Initial commit with development environment instructions
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
! .env.example
|
||||||
|
active_notes.txt
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Använd en officiell Python-image med Playwright-stöd
|
||||||
|
FROM mcr.microsoft.com/playwright/python:v1.40.0-jammy
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Kopiera requirements först för att utnyttja Docker-cache
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Installera webbläsaren Chromium (Playwright)
|
||||||
|
RUN playwright install chromium
|
||||||
|
|
||||||
|
# Kopiera resten av projektet
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Skapa tom fil för noter om den inte finns
|
||||||
|
RUN touch active_notes.txt
|
||||||
|
|
||||||
|
# Exponera Flask-porten
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Kör skriptet (unbuffered för att se loggar i docker logs)
|
||||||
|
CMD ["python3", "-u", "hedgeagent.py"]
|
||||||
76
README.md
Normal file
76
README.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# HedgeAgent
|
||||||
|
|
||||||
|
En intelligent agent som övervakar och interagerar med HedgeDoc-dokument i realtid.
|
||||||
|
|
||||||
|
## Funktioner
|
||||||
|
- **Live-chatt:** Skriv prompts direkt i dokumentet inom `<!--Agent -->`-block.
|
||||||
|
- **Tänker-indikator:** Visar animerad feedback när agenten genererar svar.
|
||||||
|
- **Webbgränssnitt:** Enkelt gränssnitt på port 5000 för att lägga till nya dokument.
|
||||||
|
- **Robust inloggning:** Automatisk återanslutning vid utgångna sessioner.
|
||||||
|
|
||||||
|
## Snabbstart med Docker
|
||||||
|
|
||||||
|
### Alternativ 1: Docker Compose (Rekommenderas)
|
||||||
|
|
||||||
|
1. **Skapa en `.env`-fil:**
|
||||||
|
```env
|
||||||
|
HEDGEDOC_EMAIL=din@epost.se
|
||||||
|
HEDGEDOC_PASSWORD=ditt lösenord
|
||||||
|
HEDGEDOC_BASE_URL=https://hedgedoc.din-doman.se
|
||||||
|
LLM_API_KEY=sk-or-v1-...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Kör med compose:**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternativ 2: Docker CLI
|
||||||
|
```bash
|
||||||
|
docker build -t hedgeagent .
|
||||||
|
docker run -d \
|
||||||
|
--name hedgeagent \
|
||||||
|
-p 5000:5000 \
|
||||||
|
--env-file .env \
|
||||||
|
-v $(pwd)/active_notes.txt:/app/active_notes.txt \
|
||||||
|
hedgeagent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utvecklingsmiljö
|
||||||
|
|
||||||
|
För att köra HedgeAgent lokalt utan Docker:
|
||||||
|
|
||||||
|
1. **Förutsättningar:**
|
||||||
|
- Python 3.10+ installerat.
|
||||||
|
|
||||||
|
2. **Skapa och aktivera en virtuell miljö:**
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # På Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Installera beroenden:**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Konfigurera miljövariabler:**
|
||||||
|
Skapa en `.env`-fil (se exemplet ovan) eller sätt dem i din shell.
|
||||||
|
|
||||||
|
5. **Starta applikationen:**
|
||||||
|
```bash
|
||||||
|
python hedgeagent.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Användning
|
||||||
|
1. Öppna webbgränssnittet på `http://localhost:5000`.
|
||||||
|
2. Klistra in URL:en till ett HedgeDoc-dokument.
|
||||||
|
3. I dokumentet, skriv din fråga efter `-> ` i ett Agent-block:
|
||||||
|
```markdown
|
||||||
|
<!--Agent
|
||||||
|
User: Hej!
|
||||||
|
Agent: Hej, vad kan jag hjälpa dig med?
|
||||||
|
-> [Skriv här och tryck Enter]
|
||||||
|
-->
|
||||||
|
```
|
||||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
hedgeagent:
|
||||||
|
build: .
|
||||||
|
container_name: hedgeagent
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./active_notes.txt:/app/active_notes.txt
|
||||||
|
restart: unless-stopped
|
||||||
390
hedgeagent.py
Normal file
390
hedgeagent.py
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import requests
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
EMAIL = os.environ.get("HEDGEDOC_EMAIL")
|
||||||
|
PASSWORD = os.environ.get("HEDGEDOC_PASSWORD")
|
||||||
|
BASE_URL = os.environ.get("HEDGEDOC_BASE_URL", "https://hedgedoc.sa6anw.se")
|
||||||
|
|
||||||
|
# LLM Configuration
|
||||||
|
LLM_API_KEY = os.environ.get("LLM_API_KEY")
|
||||||
|
LLM_MODEL = os.environ.get("LLM_MODEL", "openai/gpt-4o-mini")
|
||||||
|
LLM_API_BASE = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
|
||||||
|
AGENT_BLOCK_RE = re.compile(r"<!--Agent\n(.*?)\n-->", re.DOTALL)
|
||||||
|
NOTES_FILE = "active_notes.txt"
|
||||||
|
|
||||||
|
# Shared Queues and Flask
|
||||||
|
new_notes_queue = queue.Queue()
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# ---------- LLM Integration ----------
|
||||||
|
def call_llm(context_text, chat_history):
|
||||||
|
if not LLM_API_KEY:
|
||||||
|
return "Error: LLM_API_KEY not set"
|
||||||
|
|
||||||
|
system_prompt = (
|
||||||
|
"You are a helpful assistant integrated into a HedgeDoc markdown document.\n\n"
|
||||||
|
"DOCUMENT CONTEXT (everything outside the chat block):\n"
|
||||||
|
f"{context_text}\n\n"
|
||||||
|
"Instructions: Use the document context to answer questions. Be concise and maintain markdown formatting. "
|
||||||
|
"Each chat block in the document is independent."
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [{"role": "system", "content": system_prompt}]
|
||||||
|
|
||||||
|
for line in chat_history.splitlines():
|
||||||
|
if line.startswith("User: "):
|
||||||
|
messages.append({"role": "user", "content": line[6:].strip()})
|
||||||
|
elif line.startswith("Agent: "):
|
||||||
|
messages.append({"role": "assistant", "content": line[7:].strip()})
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
LLM_API_BASE,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {LLM_API_KEY}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"model": LLM_MODEL,
|
||||||
|
"messages": messages
|
||||||
|
},
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()['choices'][0]['message']['content']
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error calling LLM: {e}"
|
||||||
|
|
||||||
|
# ---------- Data Persistence ----------
|
||||||
|
def load_persisted_notes():
|
||||||
|
if not os.path.exists(NOTES_FILE):
|
||||||
|
return []
|
||||||
|
with open(NOTES_FILE, "r") as f:
|
||||||
|
return list(set(line.strip() for line in f if line.strip()))
|
||||||
|
|
||||||
|
def persist_note(url):
|
||||||
|
existing = load_persisted_notes()
|
||||||
|
if url not in existing:
|
||||||
|
with open(NOTES_FILE, "a") as f:
|
||||||
|
f.write(f"{url}\n")
|
||||||
|
|
||||||
|
def remove_note_from_persistence(url):
|
||||||
|
existing = load_persisted_notes()
|
||||||
|
if url in existing:
|
||||||
|
with open(NOTES_FILE, "w") as f:
|
||||||
|
for line in existing:
|
||||||
|
if line != url:
|
||||||
|
f.write(f"{line}\n")
|
||||||
|
|
||||||
|
# ---------- Web API ----------
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
from flask import render_template
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/add_note', methods=['POST'])
|
||||||
|
def add_note():
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or 'note_id' not in data:
|
||||||
|
return jsonify({"error": "Missing note_id"}), 400
|
||||||
|
|
||||||
|
note_id = data['note_id']
|
||||||
|
url = note_id if note_id.startswith("http") else f"{BASE_URL}/{note_id}"
|
||||||
|
if "?edit" not in url:
|
||||||
|
url += "?edit"
|
||||||
|
|
||||||
|
persist_note(url)
|
||||||
|
new_notes_queue.put(url)
|
||||||
|
return jsonify({"status": "added", "url": url}), 200
|
||||||
|
|
||||||
|
def start_web_server():
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)
|
||||||
|
|
||||||
|
# ---------- Hedgedoc Logic ----------
|
||||||
|
def login(page):
|
||||||
|
try:
|
||||||
|
print(f"[+] Logging in to {BASE_URL}...")
|
||||||
|
page.goto(BASE_URL, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
sign_in_btn = page.wait_for_selector('button:has-text("Sign in")', timeout=10000)
|
||||||
|
if sign_in_btn:
|
||||||
|
sign_in_btn.click()
|
||||||
|
page.wait_for_selector('input[name="email"]', timeout=5000)
|
||||||
|
page.fill('input[name="email"]', EMAIL)
|
||||||
|
page.fill('input[name="password"]', PASSWORD)
|
||||||
|
page.click('form button[type="submit"]')
|
||||||
|
page.wait_for_load_state("networkidle")
|
||||||
|
print("[✓] Login successful")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Login failed: {e}")
|
||||||
|
|
||||||
|
def is_logged_in(page):
|
||||||
|
try:
|
||||||
|
sign_in_visible = page.is_visible('button:has-text("Sign in")', timeout=2000)
|
||||||
|
return not sign_in_visible
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ensure_logged_in(context):
|
||||||
|
temp_page = context.new_page()
|
||||||
|
try:
|
||||||
|
temp_page.goto(BASE_URL, wait_until="domcontentloaded", timeout=10000)
|
||||||
|
if not is_logged_in(temp_page):
|
||||||
|
print("[!] Session expired or not logged in. Re-logging...")
|
||||||
|
login(temp_page)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Error checking login status: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
temp_page.close()
|
||||||
|
|
||||||
|
def inject_event_bridge(page):
|
||||||
|
try:
|
||||||
|
page.evaluate("""
|
||||||
|
() => {
|
||||||
|
if (window.__agentBridgeInstalled) return;
|
||||||
|
window.__agentBridgeInstalled = true;
|
||||||
|
window.__agentEvents = [];
|
||||||
|
const cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||||
|
cm.on("changes", (cm, changes) => {
|
||||||
|
window.__agentEvents.push({ ts: Date.now(), value: cm.getValue() });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Failed to inject event bridge: {e}")
|
||||||
|
|
||||||
|
def ensure_agent_block(page):
|
||||||
|
try:
|
||||||
|
text = page.evaluate("() => document.querySelector('.CodeMirror').CodeMirror.getValue()")
|
||||||
|
if "<!--Agent" not in text:
|
||||||
|
print(f"[+] Creating initial Agent block on {page.url}")
|
||||||
|
page.evaluate("""
|
||||||
|
() => {
|
||||||
|
const cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||||
|
cm.replaceRange("\\n\\n<!--Agent\\n-> \\n-->", { line: cm.lineCount(), ch: 0 });
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Error in ensure_agent_block: {e}")
|
||||||
|
|
||||||
|
def find_prompts(text: str):
|
||||||
|
prompts = []
|
||||||
|
context_parts = AGENT_BLOCK_RE.split(text)
|
||||||
|
full_context = "\n".join(part.strip() for part in context_parts if part.strip())
|
||||||
|
|
||||||
|
for m in AGENT_BLOCK_RE.finditer(text):
|
||||||
|
inner = m.group(1)
|
||||||
|
lines = inner.splitlines()
|
||||||
|
if not lines: continue
|
||||||
|
|
||||||
|
for i in range(len(lines) - 1, -1, -1):
|
||||||
|
if lines[i].strip().startswith("->"):
|
||||||
|
if i + 1 < len(lines) and lines[-1].strip() == "":
|
||||||
|
prompt_part = lines[i][2:].strip()
|
||||||
|
extra_lines = lines[i+1:-1]
|
||||||
|
full_prompt = "\n".join([prompt_part] + extra_lines).strip()
|
||||||
|
|
||||||
|
if full_prompt:
|
||||||
|
prompts.append({
|
||||||
|
"context": full_context,
|
||||||
|
"history": "\n".join(lines[:i]),
|
||||||
|
"prompt": full_prompt,
|
||||||
|
"start": m.start(),
|
||||||
|
"end": m.end(),
|
||||||
|
"original_lines": lines[:i]
|
||||||
|
})
|
||||||
|
break
|
||||||
|
return prompts
|
||||||
|
|
||||||
|
# ---------- Main Loop ----------
|
||||||
|
def main():
|
||||||
|
if not EMAIL or not PASSWORD:
|
||||||
|
print("[!] Error: HEDGEDOC_EMAIL and HEDGEDOC_PASSWORD must be set.")
|
||||||
|
return
|
||||||
|
|
||||||
|
threading.Thread(target=start_web_server, daemon=True).start()
|
||||||
|
print("[✓] Web API listening on port 5000")
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
context = browser.new_context()
|
||||||
|
|
||||||
|
auth_page = context.new_page()
|
||||||
|
login(auth_page)
|
||||||
|
auth_page.close()
|
||||||
|
|
||||||
|
pages = []
|
||||||
|
def setup_note_page(url):
|
||||||
|
try:
|
||||||
|
print(f"[+] Opening note: {url}")
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(url, wait_until="networkidle", timeout=30000)
|
||||||
|
page.keyboard.press("Control+Alt+E")
|
||||||
|
page.wait_for_selector(".CodeMirror", timeout=15000)
|
||||||
|
inject_event_bridge(page)
|
||||||
|
ensure_agent_block(page)
|
||||||
|
return page
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Failed to track {url}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
for url in load_persisted_notes():
|
||||||
|
pg = setup_note_page(url)
|
||||||
|
if pg: pages.append(pg)
|
||||||
|
|
||||||
|
print(f"[✓] Monitoring {len(pages)} notes. Ready.")
|
||||||
|
last_login_check = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if time.time() - last_login_check > 600:
|
||||||
|
if ensure_logged_in(context):
|
||||||
|
print("[✓] Re-authentication successful.")
|
||||||
|
last_login_check = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Handle new notes queue
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
url = new_notes_queue.get_nowait()
|
||||||
|
if any(p.url.split('?')[0] == url.split('?')[0] for p in pages if not p.is_closed()):
|
||||||
|
continue
|
||||||
|
pg = setup_note_page(url)
|
||||||
|
if pg: pages.append(pg)
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. Process active pages
|
||||||
|
active_pages = []
|
||||||
|
any_activity = False
|
||||||
|
for page in pages:
|
||||||
|
if page.is_closed(): continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = page.evaluate("() => document.querySelector('.CodeMirror').CodeMirror.getValue()")
|
||||||
|
except Exception:
|
||||||
|
print(f"[!] Cannot access page {page.url}, checking session...")
|
||||||
|
if not is_logged_in(page):
|
||||||
|
ensure_logged_in(context)
|
||||||
|
page.reload(wait_until="networkidle")
|
||||||
|
continue
|
||||||
|
active_pages.append(page)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "<!--Agent" not in content:
|
||||||
|
print(f"[-] Releasing {page.url}")
|
||||||
|
remove_note_from_persistence(page.url)
|
||||||
|
page.close()
|
||||||
|
continue
|
||||||
|
|
||||||
|
active_pages.append(page)
|
||||||
|
|
||||||
|
num_events = page.evaluate("() => (window.__agentEvents || []).length")
|
||||||
|
if num_events and num_events > 0:
|
||||||
|
any_activity = True
|
||||||
|
event = page.evaluate("() => window.__agentEvents.shift()")
|
||||||
|
|
||||||
|
prompts = find_prompts(event["value"])
|
||||||
|
for p in prompts:
|
||||||
|
# a. Show thinking state
|
||||||
|
thinking_text = f"<!--Agent\n" + "\n".join(p["original_lines"] + [f"User: {p['prompt']}", "Agent: 🧠 Tänker..."]) + "\n-->"
|
||||||
|
page.evaluate("""
|
||||||
|
(up) => {
|
||||||
|
const cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||||
|
const doc = cm.getDoc();
|
||||||
|
const from = doc.posFromIndex(up.start);
|
||||||
|
const to = doc.posFromIndex(up.end);
|
||||||
|
doc.replaceRange(up.new_text, from, to);
|
||||||
|
}
|
||||||
|
""", {"start": p["start"], "end": p["end"], "new_text": thinking_text})
|
||||||
|
|
||||||
|
# b. Call LLM asychronously
|
||||||
|
response_queue = queue.Queue()
|
||||||
|
def llm_worker():
|
||||||
|
try:
|
||||||
|
res = call_llm(p["context"], p["history"] + f"\nUser: {p['prompt']}")
|
||||||
|
response_queue.put(("success", res))
|
||||||
|
except Exception as e:
|
||||||
|
response_queue.put(("error", str(e)))
|
||||||
|
threading.Thread(target=llm_worker, daemon=True).start()
|
||||||
|
|
||||||
|
# c. Animation loop
|
||||||
|
dots = ""
|
||||||
|
start_time = time.time()
|
||||||
|
last_dot_time = start_time
|
||||||
|
current_block_text = thinking_text
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
status, ai_response = response_queue.get(timeout=0.5)
|
||||||
|
break
|
||||||
|
except queue.Empty:
|
||||||
|
now = time.time()
|
||||||
|
if now - start_time > 60:
|
||||||
|
status, ai_response = "error", "Timeout: Modellen svarade inte i tid."
|
||||||
|
break
|
||||||
|
if now - last_dot_time >= 5:
|
||||||
|
dots += "."
|
||||||
|
last_dot_time = now
|
||||||
|
new_thinking = f"<!--Agent\n" + "\n".join(p["original_lines"] + [f"User: {p['prompt']}", f"Agent: 🧠 Tänker{dots}"]) + "\n-->"
|
||||||
|
page.evaluate("""
|
||||||
|
(up) => {
|
||||||
|
const cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||||
|
const doc = cm.getDoc();
|
||||||
|
const from = doc.posFromIndex(up.start);
|
||||||
|
const fullText = doc.getValue();
|
||||||
|
const endIdx = fullText.indexOf("-->", up.start);
|
||||||
|
const to = (endIdx !== -1) ? doc.posFromIndex(endIdx + 3) : doc.posFromIndex(up.start + up.oldLen);
|
||||||
|
doc.replaceRange(up.new_text, from, to);
|
||||||
|
}
|
||||||
|
""", {"start": p["start"], "oldLen": len(current_block_text), "new_text": new_thinking})
|
||||||
|
current_block_text = new_thinking
|
||||||
|
|
||||||
|
# d. Show final response
|
||||||
|
if status == "error":
|
||||||
|
ai_response = f"❌ Fel: {ai_response}"
|
||||||
|
|
||||||
|
final_text = f"<!--Agent\n" + "\n".join(p["original_lines"] + [f"User: {p['prompt']}", f"Agent: {ai_response}", "-> "]) + "\n-->"
|
||||||
|
page.evaluate("""
|
||||||
|
(up) => {
|
||||||
|
const cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||||
|
const doc = cm.getDoc();
|
||||||
|
const from = doc.posFromIndex(up.start);
|
||||||
|
const fullText = doc.getValue();
|
||||||
|
const endIdx = fullText.indexOf("-->", up.start);
|
||||||
|
const to = (endIdx !== -1) ? doc.posFromIndex(endIdx + 3) : doc.posFromIndex(up.start + up.oldLen);
|
||||||
|
doc.replaceRange(up.new_text, from, to);
|
||||||
|
|
||||||
|
const lines = up.new_text.split('\\n');
|
||||||
|
let foundLine = -1;
|
||||||
|
for(let i=lines.length-1; i>=0; i--) {
|
||||||
|
if(lines[i].includes("-> ")) {
|
||||||
|
foundLine = from.line + i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundLine !== -1) {
|
||||||
|
cm.setCursor({line: foundLine, ch: 3});
|
||||||
|
}
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
|
""", {"start": p["start"], "oldLen": len(current_block_text), "new_text": final_text})
|
||||||
|
|
||||||
|
pages = active_pages
|
||||||
|
if not any_activity:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Loop error: {e}")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
playwright==1.40.0
|
||||||
|
flask==3.0.0
|
||||||
|
requests==2.31.0
|
||||||
121
templates/index.html
Normal file
121
templates/index.html
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HedgeDoc Agent - Lägg till dokument</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #f4f7f9;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
#message {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||||
|
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🤖 HedgeAgent</h1>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="note_id">Klistra in HedgeDoc-URL eller ID</label>
|
||||||
|
<input type="text" id="note_id" placeholder="https://hedgedoc.../ID" required>
|
||||||
|
</div>
|
||||||
|
<button onclick="addNote()">Lägg till dokument</button>
|
||||||
|
<div id="message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function addNote() {
|
||||||
|
const noteIdInput = document.getElementById('note_id');
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
const note_id = noteIdInput.value.trim();
|
||||||
|
|
||||||
|
if (!note_id) return;
|
||||||
|
|
||||||
|
messageDiv.style.display = 'block';
|
||||||
|
messageDiv.className = '';
|
||||||
|
messageDiv.textContent = 'Lägger till...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/add_note', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ note_id })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
messageDiv.className = 'success';
|
||||||
|
messageDiv.textContent = '✅ Dokument tillagt och övervakas nu!';
|
||||||
|
noteIdInput.value = '';
|
||||||
|
} else {
|
||||||
|
messageDiv.className = 'error';
|
||||||
|
messageDiv.textContent = '❌ Fel: ' + (result.error || 'Okänt fel');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
messageDiv.className = 'error';
|
||||||
|
messageDiv.textContent = '❌ Nätverksfel: Kunde inte kontakta agenten.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user