initial
This commit is contained in:
commit
f2e256ba90
|
|
@ -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 comma‑separated 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 comma‑separated 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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<script setup>
|
||||
defineProps({
|
||||
msg: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 & 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 right‑click (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 back‑end.
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
||||
Vue’s
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
|
@ -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))
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue