This commit is contained in:
Peter Howell 2025-02-18 10:34:50 -08:00
commit f2e256ba90
27 changed files with 3968 additions and 0 deletions

264
app.py Normal file
View File

@ -0,0 +1,264 @@
import os, re, uuid, yaml, glob, docx, PyPDF2, json
from flask import Flask, request, jsonify
INDEX_FILE = "kb_index.json"
WORD_THRESHOLD = 500 # Only generate summary/tags if text > 500 words
KB_DIR = "kb" # Directory where your Markdown notes are stored
UPLOAD_DIR = 'uploads'
os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(KB_DIR, exist_ok=True)
# ------------------------------
# LLM API Stub for Summarization & Tagging
# ------------------------------
def llm_summarize_and_tag(text):
"""
Stub for calling an LLM API.
Replace with your API call.
Returns a tuple: (summary, [tags])
"""
summary = f"Auto summary: {text[:100]}..." # Simple stub summary
tags = ["auto", "generated"]
return summary, tags
# ------------------------------
# File Extraction Helpers
# ------------------------------
def extract_text_from_pdf(file_path):
text = ""
try:
with open(file_path, "rb") as f:
reader = PyPDF2.PdfReader(f)
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
except Exception as e:
print(f"Error reading PDF {file_path}: {e}")
return text
def extract_text_from_docx(file_path):
text = ""
try:
doc = docx.Document(file_path)
for para in doc.paragraphs:
text += para.text + "\n"
except Exception as e:
print(f"Error reading DOCX {file_path}: {e}")
return text
# ------------------------------
# Segmentation Helper
# ------------------------------
def segment_text(text):
"""
Splits text into segments based on double newlines.
Returns a list of non-empty segments.
"""
segments = [seg.strip() for seg in re.split(r'\n\s*\n', text) if seg.strip()]
return segments
def load_note_from_file(file_path):
"""Load a note from a Markdown file by parsing its YAML front matter."""
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
metadata = {}
body = content
if content.startswith("---"):
# Look for YAML front matter between the first two '---' lines
match = re.search(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if match:
frontmatter_str = match.group(1)
try:
metadata = yaml.safe_load(frontmatter_str) or {}
except Exception as e:
print(f"Error parsing YAML in {file_path}: {e}")
body = content[match.end():].strip()
metadata["body"] = body
metadata["file_path"] = file_path # Used for updating an existing note
return metadata
def get_all_notes():
"""Return a list of all notes in the KB."""
notes = []
for file_path in glob.glob(os.path.join(KB_DIR, "*.md")):
note = load_note_from_file(file_path)
notes.append(note)
return notes
def get_notes_with_tag(tag):
"""Return only the notes that contain a specific tag."""
notes = get_all_notes()
filtered = [note for note in notes if "tags" in note and tag in note["tags"]]
return filtered
def get_related_tags(tag):
"""
For a given tag, return a dict of other tags that appear in the same notes
along with their frequency.
"""
notes = get_notes_with_tag(tag)
related = {}
for note in notes:
for t in note.get("tags", []):
if t != tag:
related[t] = related.get(t, 0) + 1
return related
app = Flask(__name__)
# ------------------------------
# Flask Endpoints
# ------------------------------
@app.route("/api/notes", methods=["GET"])
def api_notes():
"""
If a query parameter 'tag' is provided, return only notes with that tag.
Otherwise, return all notes.
"""
tag = request.args.get("tag")
if tag:
notes = get_notes_with_tag(tag)
else:
notes = get_all_notes()
# Remove file_path from the returned data
for note in notes:
note.pop("file_path", None)
return jsonify(notes)
@app.route("/api/related_tags", methods=["GET"])
def api_related_tags():
"""Return tags that appear with a given tag (with frequency counts)."""
tag = request.args.get("tag")
if not tag:
return jsonify({"error": "tag parameter is required"}), 400
related = get_related_tags(tag)
return jsonify(related)
@app.route("/api/note/<note_id>", methods=["GET"])
def get_note(note_id):
"""Return a single note by its ID."""
notes = get_all_notes()
for note in notes:
if note.get("id") == note_id:
note_copy = note.copy()
note_copy.pop("file_path", None)
return jsonify(note_copy)
return jsonify({"error": "Note not found"}), 404
# ------------------------------
# SAVE or update a note
# ------------------------------
@app.route("/api/note", methods=["POST"])
def save_note():
"""
Save or update a note. The expected JSON payload should contain:
- id (optional; if provided, the note is updated)
- tags (a commaseparated string)
- summary (optional)
- body (the note content)
The note is stored as a Markdown file with YAML front matter.
"""
data = request.get_json()
if not data:
return jsonify({"error": "No JSON provided"}), 400
note_id = data.get("id")
if note_id:
# Update existing note: find the file by matching note_id
notes = get_all_notes()
file_path = None
for note in notes:
if note.get("id") == note_id:
file_path = note.get("file_path")
break
if not file_path:
return jsonify({"error": "Note not found"}), 404
else:
# Create a new note
note_id = str(uuid.uuid4())
file_path = os.path.join(KB_DIR, f"{note_id}.md")
# Process tags (assumes a commaseparated string)
tags = [tag.strip() for tag in data.get("tags", "").split(",") if tag.strip()]
metadata = {
"id": note_id,
"tags": tags,
"summary": data.get("summary", "")
}
frontmatter = "---\n" + yaml.dump(metadata, default_flow_style=False) + "---\n\n"
content = frontmatter + data.get("body", "")
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
return jsonify({"status": "success", "id": note_id})
@app.route('/confirm', methods=['POST'])
def confirm():
"""
Receives a JSON payload with a file_id and reviewed segments.
For each segment longer than 500 words, automatically generate a summary and tags.
Each segment is then saved as a Markdown note with YAML front matter.
"""
data = request.get_json()
if not data:
return jsonify({"error": "No JSON provided"}), 400
file_id = data.get("file_id")
segments = data.get("segments") # List of segments with potential manual edits
if not file_id or segments is None:
return jsonify({"error": "file_id and segments are required"}), 400
note_ids = []
for segment in segments:
seg_text = segment.get("text", "")
metadata = {}
# If segment is long, generate summary and tags via LLM API
if len(seg_text.split()) > 500:
summary, auto_tags = llm_summarize_and_tag(seg_text)
metadata["summary"] = summary
metadata["tags"] = auto_tags
# Generate a unique note ID and add source reference
note_id = str(uuid.uuid4())
metadata["id"] = note_id
metadata["source_file"] = file_id
# Build Markdown content with YAML front matter
md_content = "---\n"
md_content += yaml.dump(metadata, default_flow_style=False)
md_content += "---\n\n"
md_content += seg_text
# Save the note
md_filename = os.path.join(KB_DIR, f"{note_id}.md")
with open(md_filename, "w", encoding="utf-8") as f:
f.write(md_content)
note_ids.append(note_id)
return jsonify({"status": "success", "note_ids": note_ids})
if __name__ == "__main__":
app.run(debug=True)

99
briefing.py Normal file
View File

@ -0,0 +1,99 @@
# This script gathers upcoming events, recent emails, and journal entries;
# then it searches your flat-file KB for related notes based on tags;
# finally, it compiles all the information into a briefing.
#
# (Optionally, you can send the briefing text to an LLM API for enhancement.)
import os, glob, yaml
from datetime import datetime
# Placeholder function for the LLM API call
def generate_briefing(text):
# Replace with actual API call if needed
return f"Enhanced Briefing:\n\n{text}"
def get_upcoming_events():
# In a real scenario, fetch events from a calendar source.
events = [
{"title": "Project Meeting", "datetime": "2025-02-17 10:00", "tags": ["meeting", "projectX"]},
{"title": "Code Review", "datetime": "2025-02-17 14:00", "tags": ["review", "code"]},
]
return events
def get_recent_emails():
# Simulate fetching recent emails (could be replaced with a file or API call)
emails = [
{"subject": "Follow-up on project", "content": "Let's discuss the new project approach", "tags": ["projectX"]},
]
return emails
def get_journal_entries():
# Simulate fetching journal entries (for example, from text files)
journals = [
{"date": "2025-02-15", "content": "Had a breakthrough with the complex feature."},
]
return journals
def search_kb_for_tags(tags, kb_dir="path/to/kb"):
notes = []
# Scan all Markdown files in the KB directory
for md_file in glob.glob(os.path.join(kb_dir, "*.md")):
with open(md_file, "r", encoding="utf-8") as f:
content = f.read()
if content.startswith("---"):
# Extract YAML front matter (assumes two '---' delimiters)
end = content.find("---", 3)
if end != -1:
frontmatter_str = content[3:end]
try:
metadata = yaml.safe_load(frontmatter_str)
if "tags" in metadata and any(tag in metadata["tags"] for tag in tags):
notes.append({"id": metadata.get("id", md_file), "tags": metadata["tags"], "file": md_file})
except Exception as e:
print(f"Error parsing {md_file}: {e}")
return notes
def build_briefing():
events = get_upcoming_events()
emails = get_recent_emails()
journals = get_journal_entries()
# Aggregate tags from events and emails to search the KB
tags = set()
for item in events + emails:
if "tags" in item:
tags.update(item["tags"])
kb_notes = search_kb_for_tags(tags)
# Build the briefing text
briefing_text = "Upcoming Briefing:\n\n"
briefing_text += "Events:\n"
for event in events:
briefing_text += f"- {event['datetime']}: {event['title']}\n"
briefing_text += "\nEmails:\n"
for email in emails:
briefing_text += f"- {email['subject']}: {email['content']}\n"
briefing_text += "\nJournal Entries:\n"
for journal in journals:
briefing_text += f"- {journal['date']}: {journal['content']}\n"
briefing_text += "\nRelated KB Notes:\n"
for note in kb_notes:
briefing_text += f"- Note {note['id']} with tags {', '.join(note['tags'])}\n"
# Optionally, send the briefing text to an LLM API to enhance it
enhanced_briefing = generate_briefing(briefing_text)
return enhanced_briefing
def main():
briefing = build_briefing()
with open("briefing.txt", "w", encoding="utf-8") as f:
f.write(briefing)
print("Briefing generated and saved to briefing.txt")
if __name__ == "__main__":
main()

200
indexer.py Normal file
View File

@ -0,0 +1,200 @@
import os, re, uuid, yaml, glob, docx, PyPDF2, json
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
INDEX_FILE = "kb_index.json"
WORD_THRESHOLD = 500 # Only generate summary/tags if text > 500 words
KB_DIR = "kb" # Directory where your Markdown notes are stored
UPLOAD_DIR = 'uploads'
os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(KB_DIR, exist_ok=True)
def load_index():
if os.path.exists(INDEX_FILE):
with open(INDEX_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def save_index(index):
with open(INDEX_FILE, "w", encoding="utf-8") as f:
json.dump(index, f, indent=2)
# --- Helper Functions for File Extraction ---
def extract_text_from_md(file_path):
"""
Extract YAML front matter (if any) and body text from a Markdown file.
"""
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
metadata = {}
body = content
if content.startswith("---"):
# Look for the second '---' delimiter
match = re.search(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if match:
frontmatter_str = match.group(1)
try:
metadata = yaml.safe_load(frontmatter_str) or {}
except Exception as e:
print(f"Error parsing YAML in {file_path}: {e}")
body = content[match.end():].strip()
return metadata, body
def extract_text_from_pdf(file_path):
"""
Extract text from a PDF file using PyPDF2.
"""
text = ""
try:
with open(file_path, "rb") as f:
reader = PyPDF2.PdfReader(f)
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
except Exception as e:
print(f"Error reading PDF {file_path}: {e}")
return text
def extract_text_from_docx(file_path):
"""
Extract text from a DOCX file using python-docx.
"""
text = ""
try:
doc = docx.Document(file_path)
for para in doc.paragraphs:
text += para.text + "\n"
except Exception as e:
print(f"Error reading DOCX {file_path}: {e}")
return text
def chunk_text(text, max_words=WORD_THRESHOLD):
"""
Break text into chunks of up to max_words.
Attempts to maintain coherence by splitting on word count.
"""
words = text.split()
chunks = []
for i in range(0, len(words), max_words):
chunk = " ".join(words[i:i+max_words])
chunks.append(chunk)
return chunks
# --- LLM Integration ---
def llm_summarize_and_tag(text):
"""
Stub function to call your LLM API for summarization and tag generation.
Replace this with an actual API call.
Returns a tuple: (summary, [tags])
"""
# For demonstration, we take the first 100 characters as a 'summary'
# and return some dummy tags.
summary = f"Summary: {text[:100]}..."
tags = ["auto-tag1", "auto-tag2"]
return summary, tags
# --- Processing Function ---
def process_file(file_path, index):
ext = os.path.splitext(file_path)[1].lower()
# Process Markdown notes
if ext == ".md":
metadata, body = extract_text_from_md(file_path)
# If text is long and summary is missing, generate summary and tags.
if len(body.split()) > WORD_THRESHOLD and "summary" not in metadata:
summary, auto_tags = llm_summarize_and_tag(body)
metadata["summary"] = summary
# Combine with any existing tags.
if "tags" in metadata:
metadata["tags"] = list(set(metadata["tags"] + auto_tags))
else:
metadata["tags"] = auto_tags
# Ensure an ID exists (use filename as fallback).
if "id" not in metadata:
metadata["id"] = os.path.basename(file_path)
metadata["body"] = body
index[metadata["id"]] = metadata
print(f"Indexed Markdown note: {metadata['id']}")
# Process PDFs
elif ext == ".pdf":
text = extract_text_from_pdf(file_path)
chunks = chunk_text(text, max_words=WORD_THRESHOLD)
for i, chunk in enumerate(chunks):
note_id = f"{os.path.basename(file_path)}_chunk_{i}"
note_data = {
"id": note_id,
"source": file_path,
"chunk_index": i,
"body": chunk
}
if len(chunk.split()) > WORD_THRESHOLD:
summary, auto_tags = llm_summarize_and_tag(chunk)
note_data["summary"] = summary
note_data["tags"] = auto_tags
index[note_id] = note_data
print(f"Indexed PDF chunk: {note_id}")
# Process Word documents (.doc, .docx)
elif ext in [".doc", ".docx"]:
text = extract_text_from_docx(file_path)
chunks = chunk_text(text, max_words=WORD_THRESHOLD)
for i, chunk in enumerate(chunks):
note_id = f"{os.path.basename(file_path)}_chunk_{i}"
note_data = {
"id": note_id,
"source": file_path,
"chunk_index": i,
"body": chunk
}
if len(chunk.split()) > WORD_THRESHOLD:
summary, auto_tags = llm_summarize_and_tag(chunk)
note_data["summary"] = summary
note_data["tags"] = auto_tags
index[note_id] = note_data
print(f"Indexed DOCX chunk: {note_id}")
else:
print(f"Unsupported file type: {file_path}")
# --- Watchdog Event Handler ---
class KBEventHandler(FileSystemEventHandler):
def __init__(self, index):
self.index = index
def process(self, file_path):
# Only process supported file types.
if file_path.lower().endswith((".md", ".pdf", ".doc", ".docx")):
process_file(file_path, self.index)
save_index(self.index)
def on_created(self, event):
if not event.is_directory:
self.process(event.src_path)
def on_modified(self, event):
if not event.is_directory:
self.process(event.src_path)
def main():
index = load_index()
event_handler = KBEventHandler(index)
observer = Observer()
observer.schedule(event_handler, path=KB_DIR, recursive=False)
observer.start()
print("Monitoring KB directory for changes...")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == "__main__":
main()

30
notes/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
notes/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

29
notes/README.md Normal file
View File

@ -0,0 +1,29 @@
# notes
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

13
notes/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
notes/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

2580
notes/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
notes/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "notes",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2"
}
}

BIN
notes/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

56
notes/src/App.vue Normal file
View File

@ -0,0 +1,56 @@
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
import ParagraphEditor from './components/ParagraphEditor.vue'
import NoteEditor from './components/NoteEditor.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<NoteEditor />
<!--
<HelloWorld msg="You did it!" />
-->
</div>
</header>
<main>
<!--
<TheWelcome />
-->
</main>
</template>
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
}
</style>

86
notes/src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

35
notes/src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,44 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div class="note-editor">
<h2>{{ noteId ? 'Edit Note' : 'New Note' }}</h2>
<div>
<label>Tags (comma-separated):</label>
<input v-model="tags" placeholder="e.g., idea, project" />
</div>
<div>
<label>Summary:</label>
<textarea
v-model="summary"
placeholder="Auto-generated summary if applicable"
></textarea>
</div>
<div>
<label>Body:</label>
<textarea
v-model="body"
rows="10"
placeholder="Enter your note here"
></textarea>
</div>
<button @click="saveNote">Save Note</button>
</div>
</template>
<script>
export default {
name: "NoteEditor",
props: {
noteId: {
type: String,
default: null,
},
},
data() {
return {
tags: "",
summary: "",
body: "",
};
},
methods: {
fetchNote() {
if (this.noteId) {
fetch(`/api/note/${this.noteId}`)
.then((response) => response.json())
.then((data) => {
this.tags = data.tags ? data.tags.join(", ") : "";
this.summary = data.summary || "";
this.body = data.body || "";
})
.catch((error) => {
console.error("Error fetching note:", error);
});
}
},
saveNote() {
const payload = {
id: this.noteId,
tags: this.tags,
summary: this.summary,
body: this.body,
};
fetch("/api/note", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
.then((response) => response.json())
.then((data) => {
console.log("Note saved:", data);
// Optionally, display a success message or redirect.
})
.catch((error) => {
console.error("Error saving note:", error);
});
},
},
mounted() {
this.fetchNote();
},
};
</script>
<style scoped>
.note-editor {
display: flex;
flex-direction: column;
max-width: 600px;
margin: 0 auto;
}
.note-editor label {
display: block;
margin-top: 10px;
font-weight: bold;
}
.note-editor input,
.note-editor textarea {
width: 100%;
padding: 8px;
margin-top: 4px;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="text-container" @contextmenu.prevent="handleContextMenu($event)">
<!-- This is the content you want to work with -->
<p v-for="(paragraph, index) in paragraphs" :key="index" v-html="paragraph"></p>
<!-- Context menu appears only when there is a text selection -->
<div
v-if="contextMenu.visible"
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
class="context-menu"
>
<ul>
<li @click="sendSelectedTextToLLM">
Chunk &amp; Send to LLM for Summary
</li>
</ul>
</div>
</div>
</template>
<script>
/*
How It Works
Text Display:
The component renders paragraphs of text. In a real application, you might load these dynamically.
Selection Detection & Context Menu:
When you rightclick (contextmenu event) anywhere in the text container, the code checks for any highlighted text using window.getSelection(). If any text is selected, it shows a popup menu positioned at the mouse coordinates.
Sending Selected Text:
Clicking the context menu item calls the sendSelectedTextToLLM method. This method sends the highlighted text to the /api/send_chunk endpoint via a POST request. The endpoint can then handle the background processing (e.g., LLM summarization and tagging).
Cleanup:
After sending, the component clears the selection and hides the context menu so you can continue working.
This implementation gives you a flexible tool for manually selecting text and triggering background processing on the fly. You can further refine the UI or add additional options as needed.
*/
export default {
name: 'TextSelector',
data() {
return {
// Example content; in a real app, this would come from your backend.
paragraphs: [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vel nunc eu libero vehicula blandit. Vivamus aliquam, nibh ac ultricies mattis, ligula ipsum feugiat velit, non blandit libero arcu a est.",
"Phasellus consequat velit vel dolor ullamcorper, quis lacinia orci convallis. Suspendisse potenti. Sed ac elementum mauris, ac cursus nibh. In hac habitasse platea dictumst.",
"Integer ac turpis semper, commodo mi a, facilisis orci. Mauris eget lacus eget nibh venenatis cursus. Aliquam erat volutpat. Aenean vestibulum malesuada nisi, ac sollicitudin est vestibulum non."
],
contextMenu: {
visible: false,
x: 0,
y: 0
},
selectedText: ""
};
},
methods: {
handleContextMenu(event) {
// Get the current text selection from the browser.
const selection = window.getSelection();
this.selectedText = selection ? selection.toString().trim() : "";
// Only show the context menu if some text is highlighted.
if (this.selectedText.length > 0) {
this.contextMenu.visible = true;
this.contextMenu.x = event.clientX;
this.contextMenu.y = event.clientY;
}
},
sendSelectedTextToLLM() {
if (this.selectedText.length === 0) return;
// Call the background API endpoint (adjust the URL as needed)
fetch('/api/send_chunk', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: this.selectedText })
})
.then(response => response.json())
.then(data => {
console.log('LLM processing started:', data);
// Optionally, update your UI to indicate background processing.
})
.catch(error => {
console.error('Error sending chunk:', error);
});
// Clear the selection and hide the context menu.
this.selectedText = "";
window.getSelection().removeAllRanges();
this.contextMenu.visible = false;
},
closeContextMenu() {
this.contextMenu.visible = false;
}
},
mounted() {
// Close the context menu when clicking anywhere else.
document.addEventListener('click', this.closeContextMenu);
},
beforeDestroy() {
document.removeEventListener('click', this.closeContextMenu);
}
};
</script>
<style scoped>
.text-container {
padding: 16px;
user-select: text;
}
.context-menu {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
z-index: 1000;
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2);
}
.context-menu ul {
list-style: none;
margin: 0;
padding: 4px 0;
}
.context-menu li {
padding: 8px 16px;
cursor: pointer;
white-space: nowrap;
}
.context-menu li:hover {
background-color: #f0f0f0;
}
</style>

View File

@ -0,0 +1,94 @@
<script setup>
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

6
notes/src/main.js Normal file
View File

@ -0,0 +1,6 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

18
notes/vite.config.js Normal file
View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})