mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-10 06:48:04 -05:00
Merge pull request #1309 from jmd1010/feature/web-ui-enhancements
Feature/Web Svelte GUI Enhancements: Pattern Descriptions, Tags, Favorites, Search Bar, Language Integration, PDF file conversion, etc
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -342,3 +342,8 @@ web/.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
|
||||
web/myfiles/Obsidian_perso_not_share/
|
||||
ENV
|
||||
web/package-lock.json
|
||||
.gitignore_backup
|
||||
web/static/*.png
|
||||
|
||||
9
ENV
9
ENV
@@ -1,9 +0,0 @@
|
||||
DEFAULT_VENDOR=OpenRouter
|
||||
DEFAULT_MODEL=openai/gpt-3.5-turbo-0125
|
||||
DEFAULT_MODEL_CONTEXT_LENGTH=128K
|
||||
PATTERNS_LOADER_GIT_REPO_URL=https://github.com/danielmiessler/fabric.git
|
||||
PATTERNS_LOADER_GIT_REPO_PATTERNS_FOLDER=patterns
|
||||
OPENROUTER_API_KEY=sk-or-v1-
|
||||
OPENROUTER_API_BASE_URL=https://openrouter.ai/api/v1
|
||||
YOUTUBE_API_KEY=AIzaS
|
||||
JINA_AI_API_KEY=jina_57
|
||||
124
PATTERN_DESCRIPTIONS/README_Pattern_Descriptions_and_Tags_MGT.md
Normal file
124
PATTERN_DESCRIPTIONS/README_Pattern_Descriptions_and_Tags_MGT.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Pattern Descriptions and Tags Management
|
||||
|
||||
This document explains the complete workflow for managing pattern descriptions and tags, including how to process new patterns and maintain metadata.
|
||||
|
||||
## System Overview
|
||||
|
||||
The pattern system follows this hierarchy:
|
||||
1. `~/.config/fabric/patterns/` directory: The source of truth for available patterns
|
||||
2. `pattern_extracts.json`: Contains first 500 words of each pattern for reference
|
||||
3. `pattern_descriptions.json`: Stores pattern metadata (descriptions and tags)
|
||||
4. `web/static/data/pattern_descriptions.json`: Web-accessible copy for the interface
|
||||
|
||||
## Pattern Processing Workflow
|
||||
|
||||
### 1. Adding New Patterns
|
||||
- Add patterns to `~/.config/fabric/patterns/`
|
||||
- Run extract_patterns.py to process new additions:
|
||||
```bash
|
||||
python extract_patterns.py
|
||||
|
||||
The Python Script automatically:
|
||||
- Creates pattern extracts for reference
|
||||
- Adds placeholder entries in descriptions file
|
||||
- Syncs to web interface
|
||||
|
||||
### 2. Pattern Extract Creation
|
||||
The script extracts first 500 words from each pattern's system.md file to:
|
||||
|
||||
- Provide context for writing descriptions
|
||||
- Maintain reference material
|
||||
- Aid in pattern categorization
|
||||
|
||||
### 3. Description and Tag Management
|
||||
Pattern descriptions and tags are managed in pattern_descriptions.json:
|
||||
|
||||
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"patternName": "pattern_name",
|
||||
"description": "[Description pending]",
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
## Completing Pattern Metadata
|
||||
|
||||
### Writing Descriptions
|
||||
1. Check pattern_descriptions.json for "[Description pending]" entries
|
||||
2. Reference pattern_extracts.json for context
|
||||
|
||||
3. How to update Pattern short descriptions (one sentence).
|
||||
|
||||
You can update your descriptions in pattern_descriptions.json manually or using LLM assistance (prefered approach).
|
||||
|
||||
Tell AI to look for "Description pending" entries in this file and write a short description based on the extract info in the pattern_extracts.json file. You can also ask your LLM to add tags for those newly added patterns, using other patterns tag assignments as example.
|
||||
|
||||
### Managing Tags
|
||||
1. Add appropriate tags to new patterns
|
||||
2. Update existing tags as needed
|
||||
3. Tags are stored as arrays: ["TAG1", "TAG2"]
|
||||
4. Edit pattern_descriptions.json directly to modify tags
|
||||
5. Make tags your own. You can delete, replace, amend existing tags.
|
||||
|
||||
## File Synchronization
|
||||
|
||||
The script maintains synchronization between:
|
||||
- Local pattern_descriptions.json
|
||||
- Web interface copy in static/data/
|
||||
- No manual file copying needed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Run extract_patterns.py when:
|
||||
- Adding new patterns
|
||||
- Updating existing patterns
|
||||
- Modifying pattern structure
|
||||
|
||||
2. Description Writing:
|
||||
- Use pattern extracts for context
|
||||
- Keep descriptions clear and concise
|
||||
- Focus on pattern purpose and usage
|
||||
|
||||
3. Tag Management:
|
||||
- Use consistent tag categories
|
||||
- Apply multiple tags when relevant
|
||||
- Update tags to reflect pattern evolution
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If patterns are not showing in the web interface:
|
||||
1. Verify pattern_descriptions.json format
|
||||
2. Check web static copy exists
|
||||
3. Ensure proper file permissions
|
||||
4. Run extract_patterns.py to resync
|
||||
|
||||
## File Structure
|
||||
|
||||
fabric/
|
||||
├── patterns/ # Pattern source files
|
||||
├── PATTERN_DESCRIPTIONS/
|
||||
│ ├── extract_patterns.py # Pattern processing script
|
||||
│ ├── pattern_extracts.json # Pattern content references
|
||||
│ └── pattern_descriptions.json # Pattern metadata
|
||||
└── web/
|
||||
└── static/
|
||||
└── data/
|
||||
└── pattern_descriptions.json # Web interface copy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
114
PATTERN_DESCRIPTIONS/extract_patterns.py
Normal file
114
PATTERN_DESCRIPTIONS/extract_patterns.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
|
||||
def load_existing_file(filepath):
|
||||
"""Load existing JSON file or return default structure"""
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {"patterns": []}
|
||||
|
||||
def get_pattern_extract(pattern_path):
|
||||
"""Extract first 500 words from pattern's system.md file"""
|
||||
system_md_path = os.path.join(pattern_path, "system.md")
|
||||
with open(system_md_path, 'r', encoding='utf-8') as f:
|
||||
content = ' '.join(f.read().split()[:500])
|
||||
return content
|
||||
|
||||
def extract_pattern_info():
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
patterns_dir = os.path.expanduser("~/.config/fabric/patterns")
|
||||
print(f"\nScanning patterns directory: {patterns_dir}")
|
||||
|
||||
extracts_path = os.path.join(script_dir, "pattern_extracts.json")
|
||||
descriptions_path = os.path.join(script_dir, "pattern_descriptions.json")
|
||||
|
||||
existing_extracts = load_existing_file(extracts_path)
|
||||
existing_descriptions = load_existing_file(descriptions_path)
|
||||
|
||||
existing_extract_names = {p["patternName"] for p in existing_extracts["patterns"]}
|
||||
existing_description_names = {p["patternName"] for p in existing_descriptions["patterns"]}
|
||||
print(f"Found existing patterns: {len(existing_extract_names)}")
|
||||
|
||||
new_extracts = []
|
||||
new_descriptions = []
|
||||
|
||||
|
||||
for dirname in sorted(os.listdir(patterns_dir)):
|
||||
# Only log new pattern processing
|
||||
if dirname not in existing_extract_names:
|
||||
print(f"Processing new pattern: {dirname}")
|
||||
|
||||
|
||||
pattern_path = os.path.join(patterns_dir, dirname)
|
||||
system_md_path = os.path.join(pattern_path, "system.md")
|
||||
print(f"Checking system.md at: {system_md_path}")
|
||||
|
||||
if os.path.isdir(pattern_path) and os.path.exists(system_md_path):
|
||||
print(f"Valid pattern directory found: {dirname}")
|
||||
try:
|
||||
if dirname not in existing_extract_names:
|
||||
print(f"Creating new extract for: {dirname}")
|
||||
pattern_extract = get_pattern_extract(pattern_path) # Pass directory path
|
||||
new_extracts.append({
|
||||
"patternName": dirname,
|
||||
"pattern_extract": pattern_extract
|
||||
})
|
||||
|
||||
if dirname not in existing_description_names:
|
||||
print(f"Creating new description for: {dirname}")
|
||||
new_descriptions.append({
|
||||
"patternName": dirname,
|
||||
"description": "[Description pending]",
|
||||
"tags": []
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing {dirname}: {str(e)}")
|
||||
else:
|
||||
print(f"Invalid pattern directory or missing system.md: {dirname}")
|
||||
|
||||
print(f"\nProcessing summary:")
|
||||
print(f"New extracts created: {len(new_extracts)}")
|
||||
print(f"New descriptions added: {len(new_descriptions)}")
|
||||
|
||||
existing_extracts["patterns"].extend(new_extracts)
|
||||
existing_descriptions["patterns"].extend(new_descriptions)
|
||||
|
||||
return existing_extracts, existing_descriptions, len(new_descriptions)
|
||||
|
||||
|
||||
def update_web_static(descriptions_path):
|
||||
"""Copy pattern descriptions to web static directory"""
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
static_dir = os.path.join(script_dir, "..", "web", "static", "data")
|
||||
os.makedirs(static_dir, exist_ok=True)
|
||||
static_path = os.path.join(static_dir, "pattern_descriptions.json")
|
||||
shutil.copy2(descriptions_path, static_path)
|
||||
|
||||
def save_pattern_files():
|
||||
"""Save both pattern files and sync to web"""
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
extracts_path = os.path.join(script_dir, "pattern_extracts.json")
|
||||
descriptions_path = os.path.join(script_dir, "pattern_descriptions.json")
|
||||
|
||||
pattern_extracts, pattern_descriptions, new_count = extract_pattern_info()
|
||||
|
||||
# Save files
|
||||
with open(extracts_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(pattern_extracts, f, indent=2, ensure_ascii=False)
|
||||
|
||||
with open(descriptions_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(pattern_descriptions, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Update web static
|
||||
update_web_static(descriptions_path)
|
||||
|
||||
print(f"\nProcessing complete:")
|
||||
print(f"Total patterns: {len(pattern_descriptions['patterns'])}")
|
||||
print(f"New patterns added: {new_count}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
save_pattern_files()
|
||||
|
||||
1721
PATTERN_DESCRIPTIONS/pattern_descriptions.json
Normal file
1721
PATTERN_DESCRIPTIONS/pattern_descriptions.json
Normal file
File diff suppressed because it is too large
Load Diff
828
PATTERN_DESCRIPTIONS/pattern_extracts.json
Normal file
828
PATTERN_DESCRIPTIONS/pattern_extracts.json
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,64 @@
|
||||
# Enhanced Pattern Selection and WEB UI Improvements
|
||||
|
||||
This PR adds several Web UI and functionality improvements to make pattern selection more intuitive and provide better context for each pattern's purpose.
|
||||
|
||||
## Demo
|
||||
Watch the demo video showcasing the new features: https://youtu.be/qVuKhCw_edk
|
||||
|
||||
## Major Improvements
|
||||
|
||||
### Pattern Selection and Description
|
||||
- Added modal interface for pattern selection
|
||||
- Added short pattern descriptions for each pattern
|
||||
- Added Select Pattern to execute from Modal
|
||||
- Added scroll functionality to System Instructions frame
|
||||
- **Added search functionality in pattern selection modal**
|
||||
- Real-time pattern filtering as you type
|
||||
- Case-insensitive partial name matching
|
||||
- Maintains favorites sorting while filtering
|
||||
|
||||
### User Experience
|
||||
- Implemented favorites functionality for quick access to frequently used patterns
|
||||
- Improved YouTube transcript handling
|
||||
- Enhanced UI components for better user experience
|
||||
- **Added Obsidian integration for pattern execution output**
|
||||
- Save pattern results directly to Obsidian from web interface
|
||||
- Configurable note naming
|
||||
- Seamless integration with existing Obsidian workflow
|
||||
|
||||
## Technical Improvements
|
||||
- Added backend support for new features
|
||||
- Improved pattern management and selection
|
||||
- Enhanced state management for patterns and favorites
|
||||
|
||||
## Key Files Modified
|
||||
|
||||
### Backend Changes
|
||||
- `fabric/restapi/`: Added new endpoints and functionality for pattern management
|
||||
- `chat.go`, `patterns.go`: Enhanced pattern handling
|
||||
- `configuration.go`, `models.go`: Added support for new features
|
||||
- **`obsidian.go`: New Obsidian integration endpoints**
|
||||
|
||||
### Frontend Changes
|
||||
- `fabric/web/src/lib/components/`:
|
||||
- `chat/`: Enhanced chat interface components
|
||||
- `patterns/`: New pattern selection components
|
||||
- **Added pattern search functionality**
|
||||
- **Enhanced modal UI with search capabilities**
|
||||
- `ui/modal/`: Modal interface implementation
|
||||
- `fabric/web/src/lib/store/`:
|
||||
- `favorites-store.ts`: New favorites functionality
|
||||
- `pattern-store.ts`: Enhanced pattern management
|
||||
- **`obsidian-store.ts`: New Obsidian integration state management**
|
||||
- `fabric/web/src/lib/services/`:
|
||||
- `transcriptService.ts`: Improved YouTube handling
|
||||
|
||||
### Pattern Descriptions
|
||||
- `fabric/myfiles/`:
|
||||
- `pattern_descriptions.json`: Added detailed pattern descriptions
|
||||
- `extract_patterns.py`: Tool for pattern management
|
||||
|
||||
These improvements make the pattern selection process more intuitive and provide users with better context about each pattern's purpose and functionality. The addition of pattern search and Obsidian integration further enhances the user experience by providing quick access to patterns and seamless integration with external note-taking workflows.
|
||||
|
||||
## Note on Platform Compatibility
|
||||
This implementation was developed and tested on macOS. Some modifications may not be required for Windows users, particularly around system-specific paths and configurations. Windows users may need to adjust certain paths or configurations to match their environment.
|
||||
155
WEB INTERFACE MOD README FILES/language-options.md
Normal file
155
WEB INTERFACE MOD README FILES/language-options.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Language Support Implementation
|
||||
|
||||
## Overview
|
||||
The language support allows switching between languages using qualifiers (--fr, --en) in the chat input. The implementation is simple and effective, working at multiple layers of the application.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Language Store (language-store.ts)
|
||||
```typescript
|
||||
// Manages language state
|
||||
export const languageStore = writable<string>('');
|
||||
```
|
||||
|
||||
### 2. Chat Input (ChatInput.svelte)
|
||||
- Detects language qualifiers in user input
|
||||
- Updates language store
|
||||
- Strips qualifier from message
|
||||
```typescript
|
||||
// Language qualifier handling
|
||||
if (qualifier === 'fr') {
|
||||
languageStore.set('fr');
|
||||
userInput = userInput.replace(/--fr\s*/, '');
|
||||
} else if (qualifier === 'en') {
|
||||
languageStore.set('en');
|
||||
userInput = userInput.replace(/--en\s*/, '');
|
||||
}
|
||||
|
||||
// After sending message
|
||||
try {
|
||||
await sendMessage(userInput);
|
||||
languageStore.set('en'); // Reset to default after send
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Chat Service (ChatService.ts)
|
||||
- Adds language instruction to prompts
|
||||
- Defaults to English if no language specified
|
||||
```typescript
|
||||
const language = get(languageStore) || 'en';
|
||||
const languageInstruction = language !== 'en'
|
||||
? `. Please use the language '${language}' for the output.`
|
||||
: '';
|
||||
const fullInput = userInput + languageInstruction;
|
||||
```
|
||||
|
||||
### 4. Global Settings UI (Chat.svelte)
|
||||
```typescript
|
||||
// Language selector in Global Settings
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Language</Label>
|
||||
<Select bind:value={selectedLanguage}>
|
||||
<option value="">Default</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">French</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
// Script section
|
||||
let selectedLanguage = $languageStore;
|
||||
$: languageStore.set(selectedLanguage);
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. User Input:
|
||||
- User types message with language qualifier (e.g., "--fr Hello")
|
||||
- ChatInput detects qualifier and updates language store
|
||||
- Qualifier is stripped from message
|
||||
- OR user selects language from Global Settings dropdown
|
||||
|
||||
2. Request Processing:
|
||||
- ChatService gets language from store
|
||||
- Adds language instruction to prompt
|
||||
- Sends to backend
|
||||
|
||||
3. Response:
|
||||
- AI responds in requested language
|
||||
- Response is displayed without modification
|
||||
- Language store is reset to English after message is sent
|
||||
|
||||
## Usage Examples
|
||||
|
||||
1. English (Default):
|
||||
```
|
||||
User: What is the weather?
|
||||
AI: The weather information...
|
||||
```
|
||||
|
||||
2. French:
|
||||
```
|
||||
User: --fr What is the weather?
|
||||
AI: Voici les informations météo...
|
||||
```
|
||||
|
||||
3. Using Global Settings:
|
||||
```
|
||||
1. Select "French" from language dropdown
|
||||
2. Type: What is the weather?
|
||||
3. AI responds in French
|
||||
4. Language resets to English after response
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. Simple Design:
|
||||
- No complex language detection
|
||||
- No translation layer
|
||||
- Direct instruction to AI
|
||||
|
||||
2. Stateful:
|
||||
- Language persists until changed
|
||||
- Resets to English on page refresh
|
||||
- Resets to English after each message
|
||||
|
||||
3. Extensible:
|
||||
- Easy to add new languages
|
||||
- Just add new qualifiers and store values
|
||||
- Update Global Settings dropdown options
|
||||
|
||||
4. Error Handling:
|
||||
- Invalid qualifiers are ignored
|
||||
- Unknown languages default to English
|
||||
- Store reset on error to prevent state issues
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always reset language after message:
|
||||
```typescript
|
||||
// Reset stores after successful send
|
||||
languageStore.set('en');
|
||||
```
|
||||
|
||||
2. Default to English:
|
||||
```typescript
|
||||
const language = get(languageStore) || 'en';
|
||||
```
|
||||
|
||||
3. Clear language instruction:
|
||||
```typescript
|
||||
const languageInstruction = language !== 'en'
|
||||
? `. Please use the language '${language}' for the output.`
|
||||
: '';
|
||||
```
|
||||
|
||||
4. Handle UI State:
|
||||
```typescript
|
||||
// In Chat.svelte
|
||||
let selectedLanguage = $languageStore;
|
||||
$: {
|
||||
languageStore.set(selectedLanguage);
|
||||
// Update UI immediately when store changes
|
||||
selectedLanguage = $languageStore;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# Pattern Search Implementation Plan
|
||||
|
||||
## Component Changes (PatternList.svelte)
|
||||
|
||||
### 1. Add Search Input
|
||||
```svelte
|
||||
<div class="px-4 pb-4 flex gap-4 items-center">
|
||||
<!-- Existing sort options -->
|
||||
<div class="flex-1"> <!-- Add flex-1 to push search to right -->
|
||||
<label class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input type="radio" bind:group={sortBy} value="alphabetical">
|
||||
Alphabetical
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input type="radio" bind:group={sortBy} value="favorites">
|
||||
Favorites First
|
||||
</label>
|
||||
</div>
|
||||
<!-- New search input -->
|
||||
<div class="w-48"> <!-- Fixed width for search -->
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={searchText}
|
||||
placeholder="Search patterns..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. Add Search Logic
|
||||
```typescript
|
||||
// Add to script section
|
||||
let searchText = ""; // For pattern filtering
|
||||
|
||||
// Modify sortedPatterns to include search
|
||||
$: filteredPatterns = patterns.filter(p =>
|
||||
p.patternName.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
|
||||
$: sortedPatterns = sortBy === 'alphabetical'
|
||||
? [...filteredPatterns].sort((a, b) => a.patternName.localeCompare(b.patternName))
|
||||
: [
|
||||
...filteredPatterns.filter(p => $favorites.includes(p.patternName)).sort((a, b) => a.patternName.localeCompare(b.patternName)),
|
||||
...filteredPatterns.filter(p => !$favorites.includes(p.patternName)).sort((a, b) => a.patternName.localeCompare(b.patternName))
|
||||
];
|
||||
```
|
||||
|
||||
### 3. Reset Search on Selection
|
||||
```typescript
|
||||
// In pattern selection click handler
|
||||
searchText = ""; // Reset search before closing modal
|
||||
dispatch('select', pattern.patternName);
|
||||
dispatch('close');
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Import Input component
|
||||
```typescript
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
```
|
||||
|
||||
2. Add searchText variable and filtering logic
|
||||
3. Update template to include search input
|
||||
4. Add reset logic in pattern selection handler
|
||||
5. Test search functionality:
|
||||
- Partial matches work
|
||||
- Case-insensitive search
|
||||
- Search resets on selection
|
||||
- Layout maintains consistency
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
- Search updates in real-time as user types
|
||||
- Matches are case-insensitive
|
||||
- Matches can be anywhere in pattern name
|
||||
- Search box clears when pattern is selected
|
||||
- Sort options (alphabetical/favorites) still work with filtered results
|
||||
- Maintains existing modal layout and styling
|
||||
269
WEB INTERFACE MOD README FILES/pr-1284-update.md
Normal file
269
WEB INTERFACE MOD README FILES/pr-1284-update.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Enhanced Pattern Selection, Pattern Descriptions, New Pattern TAG System, Language Support and other WEB UI Improvements V3
|
||||
|
||||
This Cummulative PR adds several Web UI and functionality improvements to make pattern selection more intuitive (pattern descriptions), ability to save favorite patterns, powerful multilingual capabilities, a Pattern TAG system, a help reference section, more robust Youtube processing and a variety of ui improvements.
|
||||
|
||||
## 🎥 Demo Video
|
||||
https://youtu.be/IhE8Iey8hSU
|
||||
|
||||
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
### 1. Web UI and Pattern Selection Improvements
|
||||
- Enhanced pattern selection interface for better user experience
|
||||
- New pattern descriptions section accessible via modal
|
||||
- New pattern favorite list and pattern search functionnality
|
||||
- New Tag system for better pattern organization and filtering
|
||||
- Web UI refinements for clearer interaction
|
||||
- Help section via modal
|
||||
|
||||
### 2. Multilingual Support System
|
||||
- Seamless language switching via UI dropdown
|
||||
- Persistent language state management
|
||||
- Pattern processing now use the selected language seamlessly
|
||||
|
||||
### 3. YouTube Integration Enhancement
|
||||
- Robust language handling for YouTube transcript processing
|
||||
- Chunk-based language maintenance for long transcripts
|
||||
- Consistent language output throughout transcript analysis
|
||||
|
||||
### 4. Enhanced Tag Management Integration
|
||||
|
||||
The tag filtering system has been deeply integrated into the Pattern Selection interface through several UI enhancements:
|
||||
|
||||
1. **Dual-Position Tag Panel**
|
||||
- Sliding panel positioned to the right of pattern modal
|
||||
- Dynamic toggle button that adapts position and text based on panel state
|
||||
- Smooth transitions for opening/closing animations
|
||||
|
||||
2. **Tag Selection Visibility**
|
||||
- New dedicated tag display section in pattern modal
|
||||
- Visual separation through subtle background styling
|
||||
- Immediate feedback showing selected tags with comma separation
|
||||
- Inline reset capability for quick tag clearing
|
||||
|
||||
3. **Improved User Experience**
|
||||
- Clear visual hierarchy between pattern list and tag filtering
|
||||
- Multiple ways to manage tags (panel or quick reset)
|
||||
- Consistent styling with existing design language
|
||||
- Space-efficient tag brick layout in 3-column grid
|
||||
|
||||
4. **Technical Implementation**
|
||||
- Reactive tag state management
|
||||
- Efficient tag filtering logic
|
||||
- Proper event dispatching between components
|
||||
- Maintained accessibility standards
|
||||
- Responsive design considerations
|
||||
|
||||
These enhancements create a more intuitive and efficient pattern discovery experience, allowing users to quickly filter and find relevant patterns while maintaining a clean, modern interface.
|
||||
|
||||
|
||||
## 🛠 Technical Implementation
|
||||
|
||||
### Language Support Architecture
|
||||
```typescript
|
||||
// Language state management
|
||||
export const languageStore = writable<string>('');
|
||||
|
||||
// Chat input language detection
|
||||
if (qualifier === 'fr') {
|
||||
languageStore.set('fr');
|
||||
userInput = userInput.replace(/--fr\s*/, '');
|
||||
}
|
||||
|
||||
// Service layer integration
|
||||
const language = get(languageStore) || 'en';
|
||||
const languageInstruction = language !== 'en'
|
||||
? `. Please use the language '${language}' for the output.`
|
||||
: '';
|
||||
```
|
||||
|
||||
### YouTube Processing Enhancement
|
||||
```typescript
|
||||
// Process stream with language instruction per chunk
|
||||
await chatService.processStream(
|
||||
stream,
|
||||
(content: string, response?: StreamResponse) => {
|
||||
if (currentLanguage !== 'en') {
|
||||
content = `${content}. Please use the language '${currentLanguage}' for the output.`;
|
||||
}
|
||||
// Update messages...
|
||||
}
|
||||
);
|
||||
```
|
||||
# Pattern Descriptions and Tags Management
|
||||
|
||||
This document explains the complete workflow for managing pattern descriptions and tags, including how to process new patterns and maintain metadata.
|
||||
|
||||
## System Overview
|
||||
|
||||
The pattern system follows this hierarchy:
|
||||
1. `~/.config/fabric/patterns/` directory: The source of truth for available patterns
|
||||
2. `pattern_extracts.json`: Contains first 500 words of each pattern for reference
|
||||
3. `pattern_descriptions.json`: Stores pattern metadata (descriptions and tags)
|
||||
4. `web/static/data/pattern_descriptions.json`: Web-accessible copy for the interface
|
||||
|
||||
## Pattern Processing Workflow
|
||||
|
||||
### 1. Adding New Patterns
|
||||
- Add patterns to `~/.config/fabric/patterns/`
|
||||
- Run extract_patterns.py to process new additions:
|
||||
```bash
|
||||
python extract_patterns.py
|
||||
|
||||
The Python Script automatically:
|
||||
- Creates pattern extracts for reference
|
||||
- Adds placeholder entries in descriptions file
|
||||
- Syncs to web interface
|
||||
|
||||
### 2. Pattern Extract Creation
|
||||
The script extracts first 500 words from each pattern's system.md file to:
|
||||
|
||||
- Provide context for writing descriptions
|
||||
- Maintain reference material
|
||||
- Aid in pattern categorization
|
||||
|
||||
### 3. Description and Tag Management
|
||||
Pattern descriptions and tags are managed in pattern_descriptions.json:
|
||||
|
||||
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"patternName": "pattern_name",
|
||||
"description": "[Description pending]",
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
## Completing Pattern Metadata
|
||||
|
||||
### Writing Descriptions
|
||||
1. Check pattern_descriptions.json for "[Description pending]" entries
|
||||
2. Reference pattern_extracts.json for context
|
||||
|
||||
3. How to update Pattern short descriptions (one sentence).
|
||||
|
||||
You can update your descriptions in pattern_descriptions.json manually or using LLM assistance (prefered approach).
|
||||
|
||||
Tell AI to look for "Description pending" entries in this file and write a short description based on the extract info in the pattern_extracts.json file. You can also ask your LLM to add tags for those newly added patterns, using other patterns tag assignments as example.
|
||||
|
||||
### Managing Tags
|
||||
1. Add appropriate tags to new patterns
|
||||
2. Update existing tags as needed
|
||||
3. Tags are stored as arrays: ["TAG1", "TAG2"]
|
||||
4. Edit pattern_descriptions.json directly to modify tags
|
||||
5. Make tags your own. You can delete, replace, amend existing tags.
|
||||
|
||||
## File Synchronization
|
||||
|
||||
The script maintains synchronization between:
|
||||
- Local pattern_descriptions.json
|
||||
- Web interface copy in static/data/
|
||||
- No manual file copying needed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Run extract_patterns.py when:
|
||||
- Adding new patterns
|
||||
- Updating existing patterns
|
||||
- Modifying pattern structure
|
||||
|
||||
2. Description Writing:
|
||||
- Use pattern extracts for context
|
||||
- Keep descriptions clear and concise
|
||||
- Focus on pattern purpose and usage
|
||||
|
||||
3. Tag Management:
|
||||
- Use consistent tag categories
|
||||
- Apply multiple tags when relevant
|
||||
- Update tags to reflect pattern evolution
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If patterns are not showing in the web interface:
|
||||
1. Verify pattern_descriptions.json format
|
||||
2. Check web static copy exists
|
||||
3. Ensure proper file permissions
|
||||
4. Run extract_patterns.py to resync
|
||||
|
||||
## File Structure
|
||||
|
||||
fabric/
|
||||
├── patterns/ # Pattern source files
|
||||
├── PATTERN_DESCRIPTIONS/
|
||||
│ ├── extract_patterns.py # Pattern processing script
|
||||
│ ├── pattern_extracts.json # Pattern content references
|
||||
│ └── pattern_descriptions.json # Pattern metadata
|
||||
└── web/
|
||||
└── static/
|
||||
└── data/
|
||||
└── pattern_descriptions.json # Web interface copy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 🎯 Usage Examples
|
||||
|
||||
### 1. Using Language Qualifiers
|
||||
```
|
||||
User: What is the weather?
|
||||
AI: The weather information...
|
||||
|
||||
User: --fr What is the weather?
|
||||
AI: Voici les informations météo...
|
||||
```
|
||||
|
||||
### 2. Global Settings
|
||||
1. Select language from dropdown
|
||||
2. All interactions use selected language
|
||||
3. Automatic reset to English after each message
|
||||
|
||||
### 3. YouTube Analysis
|
||||
```
|
||||
User: Analyze this YouTube video --fr
|
||||
AI: [Provides analysis in French, maintaining language throughout the transcript]
|
||||
```
|
||||
|
||||
## 💡 Key Benefits
|
||||
|
||||
1. **Enhanced User Experience**
|
||||
- Intuitive language switching
|
||||
- Consistent language handling
|
||||
- Seamless integration with existing features
|
||||
|
||||
2. **Robust Implementation**
|
||||
- Simple yet powerful design
|
||||
- No complex language detection needed
|
||||
- Direct AI instruction approach
|
||||
|
||||
3. **Maintainable Architecture**
|
||||
- Clean separation of concerns
|
||||
- Stateful language management
|
||||
- Easy to extend for new languages
|
||||
|
||||
4. **YouTube Integration**
|
||||
- Handles long transcripts effectively
|
||||
- Maintains language consistency
|
||||
- Robust chunk processing
|
||||
|
||||
## 🔄 Implementation Notes
|
||||
|
||||
1. **State Management**
|
||||
- Language persists until changed
|
||||
- Resets to English after each message
|
||||
- Handles UI state updates efficiently
|
||||
|
||||
2. **Error Handling**
|
||||
- Invalid qualifiers are ignored
|
||||
- Unknown languages default to English
|
||||
- Proper store reset on errors
|
||||
|
||||
3. **Best Practices**
|
||||
- Clear language instructions
|
||||
- Consistent state management
|
||||
- Robust error handling
|
||||
|
||||
@@ -191,7 +191,7 @@ func writeSSEResponse(w gin.ResponseWriter, response StreamResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectFormat(content string) string {
|
||||
/* func detectFormat(content string) string {
|
||||
if strings.HasPrefix(content, "graph TD") ||
|
||||
strings.HasPrefix(content, "gantt") ||
|
||||
strings.HasPrefix(content, "flowchart") ||
|
||||
@@ -208,4 +208,15 @@ func detectFormat(content string) string {
|
||||
return "markdown"
|
||||
}
|
||||
return "plain"
|
||||
} */
|
||||
func detectFormat(content string) string {
|
||||
if strings.HasPrefix(content, "graph TD") ||
|
||||
strings.HasPrefix(content, "gantt") ||
|
||||
strings.HasPrefix(content, "flowchart") ||
|
||||
strings.HasPrefix(content, "sequenceDiagram") ||
|
||||
strings.HasPrefix(content, "classDiagram") ||
|
||||
strings.HasPrefix(content, "stateDiagram") {
|
||||
return "mermaid"
|
||||
}
|
||||
return "markdown"
|
||||
}
|
||||
|
||||
0
web/myfiles/Fabric_obsidian/.gitkeep
Normal file
0
web/myfiles/Fabric_obsidian/.gitkeep
Normal file
0
web/myfiles/inbox/.gitkeep
Normal file
0
web/myfiles/inbox/.gitkeep
Normal file
7411
web/package-lock.json
generated
7411
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
web/src/lib/actions/clickOutside.ts
Normal file
15
web/src/lib/actions/clickOutside.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function clickOutside(node: HTMLElement, handler: () => void) {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClick, true);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener('click', handleClick, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -2,24 +2,113 @@
|
||||
import ChatInput from "./ChatInput.svelte";
|
||||
import ChatMessages from "./ChatMessages.svelte";
|
||||
import ModelConfig from "./ModelConfig.svelte";
|
||||
import Models from "./Models.svelte";
|
||||
import Patterns from "./Patterns.svelte";
|
||||
import DropdownGroup from "./DropdownGroup.svelte";
|
||||
import NoteDrawer from "$lib/components/ui/noteDrawer/NoteDrawer.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Checkbox } from "$lib/components/ui/checkbox";
|
||||
import Tooltip from "$lib/components/ui/tooltip/Tooltip.svelte";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { obsidianSettings } from "$lib/store/obsidian-store";
|
||||
import { featureFlags } from "$lib/config/features";
|
||||
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { systemPrompt, selectedPatternName } from "$lib/store/pattern-store";
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
function openDrawer() {
|
||||
drawerStore.open({});
|
||||
}
|
||||
|
||||
$: showObsidian = $featureFlags.enableObsidianIntegration;
|
||||
</script>
|
||||
|
||||
<div class="flex gap-4 p-2 w-full">
|
||||
<aside class="w-1/5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Patterns />
|
||||
<Models />
|
||||
<div class="flex gap-0 p-2 w-full h-screen">
|
||||
<!-- Left Column -->
|
||||
<aside class="w-[50%] flex flex-col gap-2 pr-2">
|
||||
<!-- Dropdowns Group -->
|
||||
<div class="bg-background/5 p-2 rounded-lg">
|
||||
<div class="rounded-lg bg-background/10">
|
||||
<DropdownGroup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Config -->
|
||||
<div class="bg-background/5 p-2 rounded-lg">
|
||||
<ModelConfig />
|
||||
</div>
|
||||
|
||||
<!-- Message Input -->
|
||||
<div class="h-[200px] bg-background/5 rounded-lg overflow-hidden">
|
||||
<ChatInput />
|
||||
</div>
|
||||
|
||||
<!-- System Instructions -->
|
||||
<div class="flex-1 min-h-0 bg-background/5 p-2 rounded-lg">
|
||||
<div class="h-full flex flex-col">
|
||||
<Textarea
|
||||
bind:value={$systemPrompt}
|
||||
readonly={true}
|
||||
placeholder="System instructions will appear here when you select a pattern..."
|
||||
class="w-full flex-1 bg-primary-800/30 rounded-lg border-none whitespace-pre-wrap overflow-y-auto resize-none text-sm scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent hover:scrollbar-thumb-white/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="w-1/2">
|
||||
<ChatInput />
|
||||
</div>
|
||||
<!-- Right Column -->
|
||||
<div class="flex flex-col w-[50%] gap-2">
|
||||
<!-- Header with Obsidian Settings -->
|
||||
<div class="flex items-center justify-between px-2 py-1">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if showObsidian}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<Checkbox
|
||||
bind:checked={$obsidianSettings.saveToObsidian}
|
||||
id="save-to-obsidian"
|
||||
class="h-3 w-3"
|
||||
/>
|
||||
<Label for="save-to-obsidian" class="text-xs text-white/70">Save to Obsidian</Label>
|
||||
</div>
|
||||
{#if $obsidianSettings.saveToObsidian}
|
||||
<Input
|
||||
id="note-name"
|
||||
bind:value={$obsidianSettings.noteName}
|
||||
placeholder="Note name..."
|
||||
class="h-6 text-xs w-48 bg-white/5 border-none focus:ring-1 ring-white/20"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" class="h-6 px-2 text-xs opacity-70 hover:opacity-100" on:click={openDrawer}>
|
||||
<Tooltip text="Take Notes" position="left">
|
||||
<span>Take Notes</span>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="w-1/2">
|
||||
<ChatMessages />
|
||||
<!-- Chat Area -->
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
<!-- Chat History -->
|
||||
<div class="flex-1 min-h-0 bg-background/5 rounded-lg overflow-hidden">
|
||||
<ChatMessages />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoteDrawer />
|
||||
|
||||
<style>
|
||||
.loading-message {
|
||||
animation: flash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,26 +2,87 @@
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { sendMessage, messageStore } from '$lib/store/chat-store';
|
||||
import { systemPrompt } from '$lib/store/pattern-store';
|
||||
import { systemPrompt, selectedPatternName } from '$lib/store/pattern-store';
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { FileButton } from '@skeletonlabs/skeleton';
|
||||
import { Paperclip, Send, FileCheck } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { getTranscript } from '$lib/services/transcriptService';
|
||||
import { ChatService } from '$lib/services/ChatService';
|
||||
// import { obsidianSettings } from '$lib/store/obsidian-store';
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
import { obsidianSettings, updateObsidianSettings } from '$lib/store/obsidian-store';
|
||||
|
||||
const chatService = new ChatService();
|
||||
let userInput = "";
|
||||
//let files: FileList;
|
||||
let isYouTubeURL = false;
|
||||
const toastStore = getToastStore();
|
||||
|
||||
let files: File[] = [];
|
||||
let files: FileList | undefined = undefined;
|
||||
let uploadedFiles: string[] = [];
|
||||
let fileContents: string[] = [];
|
||||
let isProcessingFiles = false;
|
||||
|
||||
function detectYouTubeURL(input: string): boolean {
|
||||
const youtubePattern = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)/i;
|
||||
const isYoutube = youtubePattern.test(input);
|
||||
if (isYoutube) {
|
||||
console.log('YouTube URL detected:', input);
|
||||
console.log('Current system prompt:', $systemPrompt?.length);
|
||||
console.log('Selected pattern:', $selectedPatternName);
|
||||
}
|
||||
return isYoutube;
|
||||
}
|
||||
|
||||
function handleInput(event: Event) {
|
||||
console.log('\n=== Handle Input ===');
|
||||
const target = event.target as HTMLTextAreaElement;
|
||||
userInput = target.value;
|
||||
|
||||
const currentLanguage = get(languageStore);
|
||||
|
||||
const languageQualifiers = {
|
||||
'--en': 'en',
|
||||
'--fr': 'fr',
|
||||
'--es': 'es',
|
||||
'--de': 'de',
|
||||
'--zh': 'zh',
|
||||
'--ja': 'ja'
|
||||
};
|
||||
|
||||
let detectedLang = '';
|
||||
for (const [qualifier, lang] of Object.entries(languageQualifiers)) {
|
||||
if (userInput.includes(qualifier)) {
|
||||
detectedLang = lang;
|
||||
languageStore.set(lang);
|
||||
userInput = userInput.replace(new RegExp(`${qualifier}\\s*`), '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('2. Language state:', {
|
||||
previousLanguage: currentLanguage,
|
||||
currentLanguage: get(languageStore),
|
||||
detectedOverride: detectedLang,
|
||||
inputAfterLangRemoval: userInput
|
||||
});
|
||||
|
||||
isYouTubeURL = detectYouTubeURL(userInput);
|
||||
console.log('3. URL detection:', {
|
||||
isYouTube: isYouTubeURL,
|
||||
pattern: $selectedPatternName,
|
||||
systemPromptLength: $systemPrompt?.length
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFileUpload(e: Event) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
if (uploadedFiles.length >= 5 || (uploadedFiles.length + files.length) > 5) {
|
||||
toastStore.error('Maximum 5 files allowed');
|
||||
toastStore.trigger({
|
||||
message: 'Maximum 5 files allowed',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,7 +95,10 @@
|
||||
uploadedFiles = [...uploadedFiles, file.name];
|
||||
}
|
||||
} catch (error) {
|
||||
toastStore.error('Error processing files: ' + error.message);
|
||||
toastStore.trigger({
|
||||
message: 'Error processing files: ' + (error as Error).message,
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
} finally {
|
||||
isProcessingFiles = false;
|
||||
}
|
||||
@@ -49,39 +113,170 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
async function saveToObsidian(content: string) {
|
||||
if (!$obsidianSettings.saveToObsidian) {
|
||||
console.log('Obsidian saving is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$obsidianSettings.noteName) {
|
||||
toastStore.trigger({
|
||||
message: 'Please enter a note name in Obsidian settings',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$selectedPatternName) {
|
||||
toastStore.trigger({
|
||||
message: 'No pattern selected',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
toastStore.trigger({
|
||||
message: 'No content to save',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let finalContent = "";
|
||||
if (fileContents.length > 0) {
|
||||
finalContent += '\n\nFile Contents:\n' + fileContents.map((content, index) =>
|
||||
`[${uploadedFiles[index]}]:\n${content}`
|
||||
).join('\n\n');
|
||||
const response = await fetch('/obsidian', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pattern: $selectedPatternName,
|
||||
noteName: $obsidianSettings.noteName,
|
||||
content
|
||||
})
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(responseData.error || 'Failed to save to Obsidian');
|
||||
}
|
||||
|
||||
const trimmedInput = userInput.trim() + '\n' + (finalContent || '');
|
||||
const trimmedSystemPrompt = $systemPrompt.trim();
|
||||
let messageHistory = JSON.stringify($messageStore);
|
||||
|
||||
$systemPrompt = "";
|
||||
userInput = "";
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
|
||||
// Place the messageHistory in the sendMessage(``) function to send the message history
|
||||
// This is a WIP and temporarily disabled.
|
||||
await sendMessage(`${trimmedSystemPrompt}\n${trimmedInput}\n${finalContent}`);
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
// Add this after successful save
|
||||
updateObsidianSettings({
|
||||
saveToObsidian: false, // Reset the save flag
|
||||
noteName: '' // Clear the note name
|
||||
});
|
||||
toastStore.trigger({
|
||||
message: 'Failed to send message. Please try again.',
|
||||
message: responseData.message || `Saved to Obsidian: ${responseData.fileName}`,
|
||||
background: 'variant-filled-success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save to Obsidian:', error);
|
||||
toastStore.trigger({
|
||||
message: error instanceof Error ? error.message : 'Failed to save to Obsidian',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
async function processYouTubeURL(input: string) {
|
||||
console.log('\n=== YouTube Flow Start ===');
|
||||
const originalLanguage = get(languageStore);
|
||||
|
||||
try {
|
||||
// Add processing message first
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: 'Processing YouTube video...',
|
||||
format: 'loading'
|
||||
}]);
|
||||
|
||||
// Get transcript but don't display it
|
||||
const { transcript } = await getTranscript(input);
|
||||
|
||||
// Log system prompt BEFORE createChatRequest
|
||||
console.log('System prompt BEFORE createChatRequest in YouTube flow:', $systemPrompt);
|
||||
|
||||
// Log system prompt BEFORE streamChat
|
||||
console.log(`System prompt BEFORE streamChat in YouTube flow: ${$systemPrompt}`);
|
||||
|
||||
const stream = await chatService.streamChat(transcript, $systemPrompt);
|
||||
await chatService.processStream(
|
||||
stream,
|
||||
(content, response) => {
|
||||
messageStore.update(messages => {
|
||||
const newMessages = [...messages];
|
||||
// Replace the processing message with actual content
|
||||
const lastMessage = newMessages[newMessages.length - 1];
|
||||
if (lastMessage?.format === 'loading') {
|
||||
newMessages.pop();
|
||||
}
|
||||
newMessages.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
format: response?.format
|
||||
});
|
||||
return newMessages;
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
// Handle Obsidian saving if needed
|
||||
if ($obsidianSettings.saveToObsidian) {
|
||||
let lastContent = '';
|
||||
messageStore.subscribe(messages => {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage?.role === 'assistant') {
|
||||
lastContent = lastMessage.content;
|
||||
}
|
||||
})();
|
||||
if (lastContent) await saveToObsidian(lastContent);
|
||||
}
|
||||
|
||||
userInput = "";
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
} catch (error) {
|
||||
console.error('Error processing YouTube URL:', error);
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
|
||||
try {
|
||||
console.log('\n=== Submit Handler Start ===');
|
||||
|
||||
if (isYouTubeURL) {
|
||||
console.log('2a. Starting YouTube flow');
|
||||
await processYouTubeURL(userInput);
|
||||
return;
|
||||
}
|
||||
|
||||
const finalContent = fileContents.length > 0
|
||||
? userInput + '\n\nFile Contents:\n' + fileContents.join('\n\n')
|
||||
: userInput;
|
||||
|
||||
await sendMessage(finalContent);
|
||||
|
||||
userInput = "";
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
@@ -94,60 +289,76 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<div class="flex-1 relative shadow-lg">
|
||||
<Textarea
|
||||
bind:value={$systemPrompt}
|
||||
on:input={(e) => $systemPrompt || ''}
|
||||
placeholder="Enter system instructions..."
|
||||
class="w-full h-full resize-none bg-primary-800/30 rounded-lg border-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 relative shadow-lg">
|
||||
<Textarea
|
||||
bind:value={userInput}
|
||||
on:input={(e) => userInput}
|
||||
on:keydown={handleKeydown}
|
||||
placeholder="Enter your message..."
|
||||
class="w-full h-full resize-none bg-primary-800/30 rounded-lg border-none"
|
||||
/>
|
||||
<div class="absolute bottom-5 right-2 gap-2 flex justify-end end-7">
|
||||
|
||||
<FileButton
|
||||
name="file-upload"
|
||||
button="btn variant-default"
|
||||
bind:files
|
||||
on:change={handleFileUpload}
|
||||
disabled={isProcessingFiles || uploadedFiles.length >= 5}
|
||||
>
|
||||
{#if uploadedFiles.length > 0}
|
||||
<FileCheck class="w-4 h-4" />
|
||||
{:else}
|
||||
<Paperclip class="w-4 h-4" />
|
||||
{/if}
|
||||
</FileButton>
|
||||
<div class="h-full flex flex-col p-2">
|
||||
<div class="relative flex-1 min-h-0 bg-primary-800/30 rounded-lg">
|
||||
<Textarea
|
||||
bind:value={userInput}
|
||||
on:input={handleInput}
|
||||
on:keydown={handleKeydown}
|
||||
placeholder="Enter your message (YouTube URLs will be automatically processed)..."
|
||||
class="w-full h-full resize-none bg-transparent border-none text-sm focus:ring-0 transition-colors p-3 pb-[48px]"
|
||||
/>
|
||||
<div class="absolute bottom-3 right-3 flex items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if uploadedFiles.length > 0}
|
||||
<span class="text-sm text-gray-500 space-x-2">
|
||||
<span class="text-xs text-white/70">
|
||||
{uploadedFiles.length} file{uploadedFiles.length > 1 ? 's' : ''} attached
|
||||
</span>
|
||||
{/if}
|
||||
<br>
|
||||
<FileButton
|
||||
name="file-upload"
|
||||
button="btn-icon variant-ghost"
|
||||
bind:files
|
||||
on:change={handleFileUpload}
|
||||
disabled={isProcessingFiles || uploadedFiles.length >= 5}
|
||||
class="h-10 w-10 bg-primary-800/30 hover:bg-primary-800/50 rounded-full transition-colors"
|
||||
>
|
||||
{#if uploadedFiles.length > 0}
|
||||
<FileCheck class="w-5 h-5" />
|
||||
{:else}
|
||||
<Paperclip class="w-5 h-5" />
|
||||
{/if}
|
||||
</FileButton>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
name="send"
|
||||
on:click={handleSubmit}
|
||||
disabled={isProcessingFiles || !userInput.trim()}
|
||||
class="h-10 w-10 bg-primary-800/30 hover:bg-primary-800/50 rounded-full transition-colors disabled:opacity-30"
|
||||
>
|
||||
<Send class="w-4 h-4" />
|
||||
<Send class="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flex-col {
|
||||
min-height: 0;
|
||||
}
|
||||
:global(textarea) {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||||
}
|
||||
|
||||
:global(textarea::-webkit-scrollbar) {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
:global(textarea::-webkit-scrollbar-track) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:global(textarea::-webkit-scrollbar-thumb) {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:global(textarea::-webkit-scrollbar-thumb:hover) {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
:global(textarea::selection) {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,110 @@
|
||||
<script lang="ts">
|
||||
import { chatState, errorStore, streamingStore } from '$lib/store/chat-store';
|
||||
import { afterUpdate } from 'svelte';
|
||||
import { afterUpdate, onMount } from 'svelte';
|
||||
import { toastStore } from '$lib/store/toast-store';
|
||||
import { marked } from 'marked';
|
||||
import SessionManager from './SessionManager.svelte';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { ArrowDown } from 'lucide-svelte';
|
||||
import Modal from '$lib/components/ui/modal/Modal.svelte';
|
||||
import PatternList from '$lib/components/patterns/PatternList.svelte';
|
||||
import type { Message } from '$lib/interfaces/chat-interface';
|
||||
import { get } from 'svelte/store';
|
||||
import { selectedPatternName } from '$lib/store/pattern-store';
|
||||
|
||||
let messagesContainer: HTMLDivElement;
|
||||
|
||||
afterUpdate(() => {
|
||||
let showPatternModal = false;
|
||||
|
||||
let messagesContainer: HTMLDivElement | null = null;
|
||||
let showScrollButton = false;
|
||||
let isUserMessage = false;
|
||||
|
||||
function scrollToBottom() {
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
messagesContainer.scrollTo({ top: messagesContainer.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!messagesContainer) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
|
||||
showScrollButton = scrollHeight - scrollTop - clientHeight > 100;
|
||||
}
|
||||
|
||||
// Watch for changes in messages
|
||||
$: if ($chatState.messages.length > 0) {
|
||||
const lastMessage = $chatState.messages[$chatState.messages.length - 1];
|
||||
isUserMessage = lastMessage.role === 'user';
|
||||
if (isUserMessage) {
|
||||
// Only auto-scroll on user messages
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (messagesContainer) {
|
||||
messagesContainer.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
if (messagesContainer) {
|
||||
messagesContainer.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Configure marked to be synchronous
|
||||
const renderer = new marked.Renderer();
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
renderer,
|
||||
async: false
|
||||
});
|
||||
|
||||
function renderMarkdown(content: string, isAssistant: boolean) {
|
||||
content = content.replace(/\\n/g, '\n');
|
||||
if (!isAssistant) return content;
|
||||
try {
|
||||
return marked.parse(content);
|
||||
} catch (error) {
|
||||
console.error('Error rendering markdown:', error);
|
||||
return content;
|
||||
// New shouldRenderAsMarkdown function
|
||||
function shouldRenderAsMarkdown(message: Message): boolean {
|
||||
const pattern = get(selectedPatternName);
|
||||
if (pattern && message.role === 'assistant') {
|
||||
return message.format !== 'mermaid';
|
||||
}
|
||||
}
|
||||
return message.role === 'assistant' && message.format !== 'plain';
|
||||
}
|
||||
|
||||
// Keep the original renderContent function
|
||||
function renderContent(message: Message): string {
|
||||
const content = message.content.replace(/\\n/g, '\n');
|
||||
|
||||
if (shouldRenderAsMarkdown(message)) {
|
||||
try {
|
||||
return marked.parse(content, { async: false }) as string;
|
||||
} catch (error) {
|
||||
console.error('Error rendering markdown:', error);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="bg-primary-800/30 rounded-lg flex flex-col h-full shadow-lg">
|
||||
<div class="flex justify-between items-center mb-1 mt-1 flex-none">
|
||||
<div class="flex items-center gap-2 pl-4">
|
||||
<b class="text-sm text-muted-foreground font-bold">Chat History</b>
|
||||
<div class="flex justify-between items-center p-3 flex-none border-b border-white/5">
|
||||
<div>
|
||||
<span class="text-xs text-white/70 font-medium">Chat History</span>
|
||||
</div>
|
||||
<SessionManager />
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
show={showPatternModal}
|
||||
on:close={() => showPatternModal = false}
|
||||
>
|
||||
<PatternList on:close={() => showPatternModal = false} />
|
||||
</Modal>
|
||||
|
||||
{#if $errorStore}
|
||||
<div class="error-message" transition:slide>
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4" role="alert">
|
||||
@@ -47,16 +113,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="messages-container p-4 flex-1 overflow-y-auto max-h-dvh" bind:this={messagesContainer}>
|
||||
<div class="messages-content flex flex-col gap-4">
|
||||
<div
|
||||
class="messages-container p-3 flex-1 overflow-y-auto max-h-dvh relative"
|
||||
bind:this={messagesContainer}
|
||||
>
|
||||
<div class="messages-content flex flex-col gap-3">
|
||||
{#each $chatState.messages as message}
|
||||
<div
|
||||
class="message-item {message.role === 'assistant' ? 'pl-4 bg-primary/5 rounded-lg p-2' : 'pr-4 ml-auto'}"
|
||||
class="message-item {message.role === 'system' ? 'w-full bg-blue-900/20' : message.role === 'assistant' ? 'bg-primary/5 rounded-lg p-3' : 'ml-auto'}"
|
||||
transition:fade
|
||||
class:loading-message={message.format === 'loading'}
|
||||
>
|
||||
<div class="message-header flex items-center gap-2 mb-1 {message.role === 'assistant' ? '' : 'justify-end'}">
|
||||
<span class="text-xs text-muted-foreground rounded-lg p-1 variant-glass-secondary font-bold uppercase">
|
||||
{message.role === 'assistant' ? 'AI' : 'You'}
|
||||
|
||||
|
||||
<div class="message-header flex items-center gap-2 mb-1 {message.role === 'assistant' || message.role === 'system' ? '' : 'justify-end'}">
|
||||
<span class="text-xs text-muted-foreground rounded-lg p-1 variant-glass-secondary font-bold uppercase">
|
||||
{#if message.role === 'system'}
|
||||
SYSTEM
|
||||
{:else if message.role === 'assistant'}
|
||||
AI
|
||||
{:else}
|
||||
You
|
||||
{/if}
|
||||
</span>
|
||||
{#if message.role === 'assistant' && $streamingStore}
|
||||
<span class="loading-indicator flex gap-1">
|
||||
@@ -67,9 +145,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if message.role === 'assistant'}
|
||||
<div class="prose prose-slate dark:prose-invert text-inherit prose-headings:text-inherit prose-pre:bg-primary/10 prose-pre:text-inherit text-sm max-w-none">
|
||||
{@html renderMarkdown(message.content, true)}
|
||||
{#if message.role === 'system'}
|
||||
<div class="text-blue-300 text-sm font-semibold">
|
||||
{message.content}
|
||||
</div>
|
||||
{:else if message.role === 'assistant'}
|
||||
<div class="{shouldRenderAsMarkdown(message) ? 'prose prose-slate dark:prose-invert text-inherit prose-headings:text-inherit prose-pre:bg-primary/10 prose-pre:text-inherit' : 'whitespace-pre-wrap'} text-sm max-w-none">
|
||||
{@html renderContent(message)}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="whitespace-pre-wrap text-sm">
|
||||
@@ -79,15 +161,31 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if showScrollButton}
|
||||
<button
|
||||
class="absolute bottom-4 right-4 bg-primary/20 hover:bg-primary/30 rounded-full p-2 transition-opacity"
|
||||
on:click={scrollToBottom}
|
||||
transition:fade
|
||||
>
|
||||
<ArrowDown class="w-4 h-4" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/*.chat-messages-wrapper {*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* min-height: 0;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
:global(.loading-message) {
|
||||
animation: flash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
@@ -99,7 +197,7 @@
|
||||
.messages-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
@@ -130,8 +228,8 @@
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
:global(.prose pre) {
|
||||
|
||||
35
web/src/lib/components/chat/DropdownGroup.svelte
Normal file
35
web/src/lib/components/chat/DropdownGroup.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import Patterns from "./Patterns.svelte";
|
||||
import Models from "./Models.svelte";
|
||||
import { Select } from "$lib/components/ui/select";
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
|
||||
const languages = [
|
||||
{ code: '', name: 'Default Language' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'fr', name: 'French' },
|
||||
{ code: 'es', name: 'Spanish' },
|
||||
{ code: 'de', name: 'German' },
|
||||
{ code: 'zh', name: 'Chinese' },
|
||||
{ code: 'ja', name: 'Japanese' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="w-[50%]">
|
||||
<Patterns />
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<Models />
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<Select
|
||||
bind:value={$languageStore}
|
||||
class="bg-primary-800/30 border-none hover:bg-primary-800/40 transition-colors"
|
||||
>
|
||||
{#each languages as lang}
|
||||
<option value={lang.code}>{lang.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,92 +1,94 @@
|
||||
<script lang="ts">
|
||||
export {};
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Slider } from "$lib/components/ui/slider";
|
||||
import { modelConfig } from "$lib/store/model-store";
|
||||
import Transcripts from "./Transcripts.svelte";
|
||||
import NoteDrawer from '$lib/components/ui/noteDrawer/NoteDrawer.svelte';
|
||||
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { page } from '$app/stores';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { browser } from '$app/environment';
|
||||
import { clickOutside } from '$lib/actions/clickOutside';
|
||||
import Tooltip from "$lib/components/ui/tooltip/Tooltip.svelte";
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
function openDrawer() {
|
||||
drawerStore.open({});
|
||||
// Load expanded state from localStorage
|
||||
const STORAGE_KEY = 'modelConfigExpanded';
|
||||
let isExpanded = false;
|
||||
if (browser) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
isExpanded = stored ? JSON.parse(stored) : false;
|
||||
}
|
||||
|
||||
beforeNavigate(() => {
|
||||
drawerStore.close();
|
||||
});
|
||||
// Save expanded state
|
||||
function toggleExpanded() {
|
||||
isExpanded = !isExpanded;
|
||||
saveState();
|
||||
}
|
||||
|
||||
$: isVisible = $page.url.pathname.startsWith('/chat');
|
||||
function saveState() {
|
||||
if (browser) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(isExpanded));
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside() {
|
||||
if (isExpanded) {
|
||||
isExpanded = false;
|
||||
saveState();
|
||||
}
|
||||
}
|
||||
|
||||
const settings = [
|
||||
{ key: 'maxLength', label: 'Maximum Length', min: 1, max: 4000, step: 1, tooltip: "Maximum number of tokens in the response" },
|
||||
{ key: 'temperature', label: 'Temperature', min: 0, max: 2, step: 0.1, tooltip: "Higher values make output more random, lower values more focused" },
|
||||
{ key: 'top_p', label: 'Top P', min: 0, max: 1, step: 0.01, tooltip: "Controls diversity via nucleus sampling" },
|
||||
{ key: 'frequency', label: 'Frequency Penalty', min: 0, max: 1, step: 0.01, tooltip: "Reduces repetition of the same words" },
|
||||
{ key: 'presence', label: 'Presence Penalty', min: 0, max: 1, step: 0.01, tooltip: "Reduces repetition of similar topics" }
|
||||
] as const;
|
||||
</script>
|
||||
|
||||
<div class="p-2">
|
||||
<div class="space-y-1">
|
||||
<Label>Maximum Length ({$modelConfig.maxLength})</Label>
|
||||
<Slider
|
||||
bind:value={$modelConfig.maxLength}
|
||||
min={1}
|
||||
max={4000}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full" use:clickOutside={handleClickOutside}>
|
||||
<button
|
||||
class="w-full flex items-center py-2 px-2 hover:text-white/90 transition-colors rounded-t"
|
||||
on:click={toggleExpanded}
|
||||
>
|
||||
<span class="text-sm font-semibold">Model Configuration</span>
|
||||
<span class="transform transition-transform duration-200 opacity-70 ml-1 text-xs" class:rotate-180={isExpanded}>
|
||||
▼
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label>Temperature ({$modelConfig.temperature.toFixed(1)})</Label>
|
||||
<Slider
|
||||
bind:value={$modelConfig.temperature}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label>Top P ({$modelConfig.top_p.toFixed(2)})</Label>
|
||||
<Slider
|
||||
bind:value={$modelConfig.top_p}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label>Frequency Penalty ({$modelConfig.frequency.toFixed(2)})</Label>
|
||||
<Slider
|
||||
bind:value={$modelConfig.frequency}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label>Presence Penalty ({$modelConfig.presence.toFixed(2)})</Label>
|
||||
<Slider
|
||||
bind:value={$modelConfig.presence}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
<br>
|
||||
<div class="space-y-1">
|
||||
<Transcripts />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if isVisible}
|
||||
<div class="flex text-inherit justify-start mt-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
class="btn border variant-filled-primary text-align-center"
|
||||
on:click={openDrawer}
|
||||
>Open Drawer
|
||||
</Button>
|
||||
{#if isExpanded}
|
||||
<div
|
||||
class="pt-2 px-2 space-y-3"
|
||||
transition:slide={{
|
||||
duration: 200,
|
||||
easing: cubicOut,
|
||||
}}
|
||||
>
|
||||
{#each settings as setting}
|
||||
<div class="group">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<Tooltip text={setting.tooltip} position="right">
|
||||
<Label class="text-[10px] text-white/70 cursor-help group-hover:text-white/90 transition-colors">{setting.label}</Label>
|
||||
</Tooltip>
|
||||
<span class="text-[10px] font-mono text-white/50 group-hover:text-white/70 transition-colors">
|
||||
{typeof $modelConfig[setting.key] === 'number' ? $modelConfig[setting.key].toFixed(2) : $modelConfig[setting.key]}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
bind:value={$modelConfig[setting.key]}
|
||||
min={setting.min}
|
||||
max={setting.max}
|
||||
step={setting.step}
|
||||
class="h-3 group-hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
<NoteDrawer />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.slider) {
|
||||
height: 0.75rem !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
<Select
|
||||
<Select
|
||||
bind:value={$modelConfig.model}
|
||||
class="bg-primary-800/30 border-none hover:bg-primary-800/40 transition-colors"
|
||||
>
|
||||
<option value="">Default Model</option>
|
||||
{#each $availableModels as model (model.name)}
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Select } from "$lib/components/ui/select";
|
||||
import { patterns, patternAPI } from "$lib/store/pattern-store";
|
||||
import { patterns, patternAPI, systemPrompt, selectedPatternName } from "$lib/store/pattern-store";
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
let selectedPreset = "";
|
||||
let selectedPreset = $selectedPatternName || "";
|
||||
|
||||
// Subscribe to selectedPatternName changes
|
||||
selectedPatternName.subscribe(value => {
|
||||
if (value && value !== selectedPreset) {
|
||||
console.log('Pattern selected from modal:', value);
|
||||
selectedPreset = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Watch selectedPreset changes
|
||||
$: if (selectedPreset) {
|
||||
console.log('Pattern selected:', selectedPreset);
|
||||
patternAPI.selectPattern(selectedPreset);
|
||||
console.log('Pattern selected from dropdown:', selectedPreset);
|
||||
try {
|
||||
patternAPI.selectPattern(selectedPreset);
|
||||
// Verify the selection
|
||||
const currentSystemPrompt = get(systemPrompt);
|
||||
const currentPattern = get(selectedPatternName);
|
||||
console.log('After dropdown selection - Pattern:', currentPattern);
|
||||
console.log('After dropdown selection - System Prompt length:', currentSystemPrompt?.length);
|
||||
|
||||
if (!currentPattern || !currentSystemPrompt) {
|
||||
console.error('Pattern selection verification failed:');
|
||||
console.error('- Selected Pattern:', currentPattern);
|
||||
console.error('- System Prompt:', currentSystemPrompt);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in pattern selection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
@@ -16,12 +41,13 @@
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
<Select
|
||||
<Select
|
||||
bind:value={selectedPreset}
|
||||
>
|
||||
class="bg-primary-800/30 border-none hover:bg-primary-800/40 transition-colors"
|
||||
>
|
||||
<option value="">Load a pattern...</option>
|
||||
{#each $patterns as pattern}
|
||||
<option value={pattern.Name}>{pattern.Description}</option>
|
||||
<option value={pattern.Name}>{pattern.Name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang='ts'>
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Input from '$lib/components/ui/input/Input.svelte';
|
||||
import { Toast } from '@skeletonlabs/skeleton';
|
||||
|
||||
let url = '';
|
||||
@@ -75,7 +75,6 @@
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder="YouTube URL"
|
||||
class="flex-1 rounded-full border bg-background px-4"
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Sun, Moon, Menu, X, Github } from 'lucide-svelte';
|
||||
import { Sun, Moon, Menu, X, Github, FileText } from 'lucide-svelte';
|
||||
import { Avatar } from '@skeletonlabs/skeleton';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { theme, cycleTheme, initTheme } from '$lib/store/theme-store';
|
||||
// import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import Modal from '$lib/components/ui/modal/Modal.svelte';
|
||||
import PatternList from '$lib/components/patterns/PatternList.svelte';
|
||||
import HelpModal from '$lib/components/ui/help/HelpModal.svelte';
|
||||
import { selectedPatternName } from '$lib/store/pattern-store';
|
||||
|
||||
let isMenuOpen = false;
|
||||
let showPatternModal = false;
|
||||
let showHelpModal = false;
|
||||
|
||||
function goToGithub() {
|
||||
window.open('https://github.com/danielmiessler/fabric', '_blank');
|
||||
@@ -66,6 +71,15 @@
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button name="pattern-description"
|
||||
on:click={() => showPatternModal = true}
|
||||
class="inline-flex h-9 items-center justify-center rounded-full border bg-background px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground gap-2"
|
||||
aria-label="Pattern Description"
|
||||
>
|
||||
<FileText class="h-4 w-4" />
|
||||
<span>Pattern Description</span>
|
||||
</button>
|
||||
|
||||
<button name="github"
|
||||
on:click={goToGithub}
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
@@ -88,6 +102,15 @@
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</button>
|
||||
|
||||
<button name="help"
|
||||
on:click={() => showHelpModal = true}
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground ml-3"
|
||||
aria-label="Help"
|
||||
>
|
||||
<span class="text-xl font-bold text-white/90 hover:text-white">?</span>
|
||||
<span class="sr-only">Help</span>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button name="toggle-menu"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-lg border bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground md:hidden"
|
||||
@@ -121,3 +144,25 @@
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<Modal
|
||||
show={showPatternModal}
|
||||
on:close={() => showPatternModal = false}
|
||||
>
|
||||
<PatternList
|
||||
on:close={() => showPatternModal = false}
|
||||
on:select={(e) => {
|
||||
selectedPatternName.set(e.detail);
|
||||
showPatternModal = false;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
show={showHelpModal}
|
||||
on:close={() => showHelpModal = false}
|
||||
>
|
||||
<HelpModal
|
||||
on:close={() => showHelpModal = false}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
209
web/src/lib/components/patterns/PatternList.svelte
Normal file
209
web/src/lib/components/patterns/PatternList.svelte
Normal file
@@ -0,0 +1,209 @@
|
||||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Pattern } from '$lib/interfaces/pattern-interface';
|
||||
import { favorites } from '$lib/store/favorites-store';
|
||||
import { patterns, patternAPI, systemPrompt, selectedPatternName } from '$lib/store/pattern-store';
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import TagFilterPanel from './TagFilterPanel.svelte';
|
||||
|
||||
let tagFilterRef: TagFilterPanel;
|
||||
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
select: string;
|
||||
tagsChanged: string[]; // Add this line
|
||||
}>();
|
||||
|
||||
|
||||
let patternsContainer: HTMLDivElement;
|
||||
let sortBy: 'alphabetical' | 'favorites' = 'alphabetical';
|
||||
let searchText = ""; // For pattern filtering
|
||||
let selectedTags: string[] = [];
|
||||
|
||||
// First filter patterns by both text and tags
|
||||
// First filter patterns by both text and tags
|
||||
$: filteredPatterns = $patterns
|
||||
.filter((p: Pattern) =>
|
||||
p.Name.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
.filter((p: Pattern) =>
|
||||
selectedTags.length === 0 ||
|
||||
(p.tags && selectedTags.every(tag => p.tags.includes(tag)))
|
||||
);
|
||||
|
||||
// Then sort the filtered patterns
|
||||
$: sortedPatterns = sortBy === 'alphabetical'
|
||||
? [...filteredPatterns].sort((a: Pattern, b: Pattern) => a.Name.localeCompare(b.Name))
|
||||
: [
|
||||
...filteredPatterns.filter((p: Pattern) => $favorites.includes(p.Name)).sort((a: Pattern, b: Pattern) => a.Name.localeCompare(b.Name)),
|
||||
...filteredPatterns.filter((p: Pattern) => !$favorites.includes(p.Name)).sort((a: Pattern, b: Pattern) => a.Name.localeCompare(b.Name))
|
||||
];
|
||||
|
||||
|
||||
function handleTagFilter(event: CustomEvent<string[]>) {
|
||||
selectedTags = event.detail;
|
||||
}
|
||||
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await patternAPI.loadPatterns();
|
||||
} catch (error) {
|
||||
console.error('Error loading patterns:', error);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleFavorite(name: string) {
|
||||
favorites.toggleFavorite(name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="bg-primary-800 rounded-lg flex flex-col h-[85vh] w-[600px] shadow-lg relative">
|
||||
|
||||
<div class="flex flex-col border-b border-primary-700/30">
|
||||
<div class="flex justify-between items-center p-4">
|
||||
<b class="text-lg text-muted-foreground font-bold">Pattern Descriptions</b>
|
||||
<button
|
||||
on:click={() => dispatch('close')}
|
||||
class="text-muted-foreground hover:text-primary-300 transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-4 flex items-center justify-between">
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={sortBy}
|
||||
value="alphabetical"
|
||||
class="radio"
|
||||
>
|
||||
Alphabetical
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={sortBy}
|
||||
value="favorites"
|
||||
class="radio"
|
||||
>
|
||||
Favorites First
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-64 mr-4">
|
||||
<Input
|
||||
bind:value={searchText}
|
||||
placeholder="Search patterns..."
|
||||
class="text-emerald-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New tag display section -->
|
||||
<div class="px-4 pb-2">
|
||||
<div class="text-sm text-white/70 bg-primary-700/30 rounded-md p-2 flex justify-between items-center">
|
||||
<div>Tags: {selectedTags.length ? selectedTags.join(', ') : 'none'}</div>
|
||||
<button
|
||||
class="px-2 py-1 text-xs text-white/70 bg-primary-600/30 rounded hover:bg-primary-600/50 transition-colors"
|
||||
on:click={() => {
|
||||
selectedTags = [];
|
||||
dispatch('tagsChanged', selectedTags);
|
||||
}}
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<TagFilterPanel
|
||||
patterns={$patterns}
|
||||
on:tagsChanged={handleTagFilter}
|
||||
bind:this={tagFilterRef}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="patterns-container p-4 flex-1 overflow-y-auto"
|
||||
bind:this={patternsContainer}
|
||||
>
|
||||
<div class="patterns-list space-y-2">
|
||||
{#each sortedPatterns as pattern}
|
||||
<div class="pattern-item bg-primary/10 rounded-lg p-3">
|
||||
<div class="flex justify-between items-start gap-4 mb-2">
|
||||
<button
|
||||
class="text-xl font-bold text-primary-300 hover:text-primary-100 cursor-pointer transition-colors text-left w-full"
|
||||
on:click={() => {
|
||||
console.log('Selecting pattern:', pattern.Name);
|
||||
patternAPI.selectPattern(pattern.Name);
|
||||
searchText = "";
|
||||
tagFilterRef.reset();
|
||||
dispatch('select', pattern.Name);
|
||||
dispatch('close');
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.currentTarget.click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pattern.Name}
|
||||
</button>
|
||||
<button
|
||||
class="text-muted-foreground hover:text-primary-300 transition-colors"
|
||||
on:click={() => toggleFavorite(pattern.Name)}
|
||||
>
|
||||
{#if $favorites.includes(pattern.Name)}
|
||||
★
|
||||
{:else}
|
||||
☆
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground break-words leading-relaxed">{pattern.Description}</p>
|
||||
</div>
|
||||
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.patterns-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
-ms-overflow-style: thin;
|
||||
}
|
||||
|
||||
.patterns-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pattern-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.pattern-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
194
web/src/lib/components/patterns/TagFilterPanel.svelte
Normal file
194
web/src/lib/components/patterns/TagFilterPanel.svelte
Normal file
@@ -0,0 +1,194 @@
|
||||
<script lang="ts">
|
||||
import type { Pattern } from '$lib/interfaces/pattern-interface';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
tagsChanged: string[];
|
||||
}>();
|
||||
|
||||
export let patterns: Pattern[];
|
||||
let selectedTags: string[] = [];
|
||||
let isExpanded = false;
|
||||
|
||||
// Add console log to see what tags we're getting
|
||||
$: console.log('Available tags:', Array.from(new Set(patterns.flatMap(p => p.tags))));
|
||||
|
||||
// Add these debug logs
|
||||
$: console.log('Patterns received:', patterns);
|
||||
$: console.log('Tags extracted:', patterns.map(p => p.tags));
|
||||
$: console.log('Panel expanded:', isExpanded);
|
||||
|
||||
function toggleTag(tag: string) {
|
||||
selectedTags = selectedTags.includes(tag)
|
||||
? selectedTags.filter(t => t !== tag)
|
||||
: [...selectedTags, tag];
|
||||
dispatch('tagsChanged', selectedTags);
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
isExpanded = !isExpanded;
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
selectedTags = [];
|
||||
isExpanded = false;
|
||||
dispatch('tagsChanged', selectedTags);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="tag-panel {isExpanded ? 'expanded' : ''}" style="z-index: 50">
|
||||
<div class="panel-header">
|
||||
<button class="close-btn" on:click={togglePanel}>
|
||||
{isExpanded ? 'Close Filter Tags ◀' : 'Open Filter Tags ▶'}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<div class="reset-container">
|
||||
<button
|
||||
class="reset-btn"
|
||||
on:click={() => {
|
||||
selectedTags = [];
|
||||
dispatch('tagsChanged', selectedTags);
|
||||
}}
|
||||
>
|
||||
Reset All Tags
|
||||
</button>
|
||||
</div>
|
||||
{#each Array.from(new Set(patterns.flatMap(p => p.tags))).sort() as tag}
|
||||
<button
|
||||
class="tag-brick {selectedTags.includes(tag) ? 'selected' : ''}"
|
||||
on:click={() => toggleTag(tag)}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.tag-panel {
|
||||
position: fixed; /* Change to fixed positioning */
|
||||
left: calc(50% + 300px); /* Position starts after modal's right edge */
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 300px;
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
.tag-panel.expanded {
|
||||
left: calc(50% + 360px); /* Final position just to the right of modal */
|
||||
}
|
||||
|
||||
|
||||
.panel-content {
|
||||
display: none;
|
||||
padding: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.tag-brick {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.reset-container {
|
||||
width: 100%;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--primary-300);
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.expanded .panel-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
/* .toggle-btn {
|
||||
position: absolute;
|
||||
left: -30px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 8px;
|
||||
background: var(--primary-800);
|
||||
border-radius: 4px 0 0 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: -2px 0 5px rgba(0,0,0,0.2);
|
||||
} */
|
||||
|
||||
|
||||
.panel-header {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: auto;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
font-size: 0.8rem;
|
||||
color: var(--primary-300);
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Position for 'Open Filter Tags' */
|
||||
.tag-panel:not(.expanded) .close-btn {
|
||||
top: -290px; /* Moves up to search bar level */
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* Position for 'Close Filter Tags' */
|
||||
.expanded .close-btn {
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-left: -50px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tag-brick.selected {
|
||||
background: var(--primary-300);
|
||||
}
|
||||
</style>
|
||||
|
||||
27
web/src/lib/components/settings/LanguageSelector.svelte
Normal file
27
web/src/lib/components/settings/LanguageSelector.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Select } from "$lib/components/ui/select";
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
|
||||
let selectedLanguage = $languageStore;
|
||||
$: languageStore.set(selectedLanguage);
|
||||
|
||||
const languages = [
|
||||
{ code: '', name: 'Default' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'fr', name: 'French' },
|
||||
{ code: 'es', name: 'Spanish' },
|
||||
{ code: 'de', name: 'German' },
|
||||
{ code: 'zh', name: 'Chinese' },
|
||||
{ code: 'ja', name: 'Japanese' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Language</Label>
|
||||
<Select bind:value={selectedLanguage}>
|
||||
{#each languages as lang}
|
||||
<option value={lang.code}>{lang.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
@@ -2,9 +2,9 @@
|
||||
import { cn } from "$lib/utils/utils";
|
||||
import { buttonVariants } from "./index.js";
|
||||
|
||||
let className = undefined;
|
||||
export let variant = "";
|
||||
export let size = "default";
|
||||
let className: string | undefined = undefined;
|
||||
export let variant: string = "";
|
||||
export let size: string = "default";
|
||||
export { className as class };
|
||||
|
||||
$: classes = cn(
|
||||
|
||||
22
web/src/lib/components/ui/checkbox/Checkbox.svelte
Normal file
22
web/src/lib/components/ui/checkbox/Checkbox.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils";
|
||||
|
||||
export let checked: boolean = false;
|
||||
export let id: string | undefined = undefined;
|
||||
export let disabled: boolean = false;
|
||||
let className: string | undefined = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
{id}
|
||||
bind:checked
|
||||
{disabled}
|
||||
class={cn(
|
||||
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
4
web/src/lib/components/ui/checkbox/index.ts
Normal file
4
web/src/lib/components/ui/checkbox/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Checkbox from './Checkbox.svelte';
|
||||
|
||||
export { Checkbox };
|
||||
export default Checkbox;
|
||||
168
web/src/lib/components/ui/help/HelpModal.svelte
Normal file
168
web/src/lib/components/ui/help/HelpModal.svelte
Normal file
@@ -0,0 +1,168 @@
|
||||
<script lang="ts">
|
||||
import { X } from 'lucide-svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
}>();
|
||||
|
||||
let modalContent: HTMLDivElement;
|
||||
|
||||
function handleMouseLeave() {
|
||||
dispatch('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div class="bg-primary-800 rounded-lg flex flex-col h-[85vh] w-[600px] shadow-lg">
|
||||
<div class="flex justify-between items-center p-6 border-b-2 border-primary-700">
|
||||
<h1 class="text-3xl font-bold text-primary-300">Help</h1>
|
||||
<button class="text-muted-foreground hover:text-primary-300 transition-colors" on:click={() => dispatch('close')}>✕</button>
|
||||
</div>
|
||||
<div class="p-6 flex-1 overflow-y-auto" bind:this={modalContent}>
|
||||
<div class="space-y-4">
|
||||
<section>
|
||||
<h3 class="text-base font-bold text-primary-300 mb-2">Getting Started</h3>
|
||||
<p class="text-sm text-muted-foreground">Click on "Pattern Description" at the top to select a pattern or select a pattern directly from the dropdown menu.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-bold text-primary-300 mb-2">YouTube & other website URL</h3>
|
||||
<p class="text-sm text-muted-foreground">Paste a YouTube URL or an article URL in the message box and the link will be processed automatically. Youtube transcripts will be fetch and website html will be converted to markdown for LLM processing. Make sure to setup your Jina API key.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-bold text-primary-300 mb-2">Language Support</h3>
|
||||
<p class="text-sm text-muted-foreground">Select your preferred language from the dropdown menu. The AI will respond in the selected language.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-bold text-primary-300 mb-2">Model Configuration</h3>
|
||||
<p class="text-sm text-muted-foreground">Adjust model parameters like temperature and max length to control the AI's output. Higher temperature means more creative but potentially less focused responses.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-bold text-primary-300 mb-2">I only want a transcript or quick summary</h3>
|
||||
<p class="text-sm text-muted-foreground">Then don't select a pattern and simply ask in the message box "Give me a transcript" and paste the URL. it's that simple.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-bold text-primary-300 mb-2">File Support</h3>
|
||||
<p class="text-sm text-muted-foreground">You can attach files to your messages using the paperclip icon. The AI will analyze the content of text files.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-bold text-primary-300 mb-2">Taking notes and saving to Obsidian</h3>
|
||||
<p class="text-sm text-muted-foreground">By default, your personal folders should be located exactly here: web/myfiles/Fabric_obsidian and web/myfiles/inbox for notes. </p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-bold text-primary-300 mb-2">Managing Pattern Descriptions and Tags</h3>
|
||||
<p class="text-sm text-muted-foreground">Refer to instructions in the WEB INTERFACE MOD README FILES folder. </p>
|
||||
</section>
|
||||
|
||||
<section class="mt-12 pt-8 border-t-2 border-primary-700">
|
||||
<h1 class="text-2xl font-bold text-primary-300 mb-4">PATTERN TAGS</h1>
|
||||
<p class="text-sm text-muted-foreground mb-6">You can configure the TAGs as you wish and modify or replace these TAGs with yours.</p>
|
||||
<div class="text-sm text-muted-foreground space-y-4">
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">SECURITY</p>
|
||||
<p>Any pattern pertaining to IT Security</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">WRITING</p>
|
||||
<p>Any pattern pertaining to writing, editing, documentation, communication</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">ANALYSIS</p>
|
||||
<p>For patterns like analyze_paper, analyze_claims, analyze_debate</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">VISUALIZATION</p>
|
||||
<p>Any pattern involving some visual representation of something: For example, create logo, presentation diagrams etc.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">DEVELOPMENT</p>
|
||||
<p>Any pattern related to software development. For example, patterns like create_coding_project, create_prd, write_pull-request. Covers software development, coding, technical documentation</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">SUMMARIZATION</p>
|
||||
<p>Covers content condensing and key point extraction. For patterns like create_5_sentence_summary, summarize_meeting, create_micro_summary</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">EXTRACTION</p>
|
||||
<p>For patterns where key focus is mining specific information from content. For example, extract_wisdom, extract_skills, extract_patterns.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">STRATEGY</p>
|
||||
<p>For patterns like analyze_military_strategy, prepare_7s_strategy. Covers planning, decision-making frameworks, mostly with business focus.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">LEARNING</p>
|
||||
<p>Covers educational content and teaching. For patterns like to_flashcards, create_quiz, explain_math</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">CONVERSION</p>
|
||||
<p>Covers format and language transformation. For patterns like convert_to_markdown, translate, sanitize_broken_html_to_markdown</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">REVIEW</p>
|
||||
<p>Covers evaluation and assessment of source. For patterns like review_design, rate_content, analyze_presentation</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">BUSINESS</p>
|
||||
<p>Pattern aiming to support a business or entrepreneurial aim. For patterns like create_hormozi_offer, extract_business_ideas</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">AI</p>
|
||||
<p>For patterns like improve_prompt, rate_ai_response. Covers AI-specific interactions</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">GAMING</p>
|
||||
<p>For patterns like create_npc, create_rpg_summary. Covers gaming and simulation content</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">RESEARCH</p>
|
||||
<p>For patterns like analyze_paper, create_academic_paper. Covers academic and scientific content</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">CRITICAL THINKING</p>
|
||||
<p>Any pattern aiming to improve someone's thinking or aiming to assess evidence.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">SELF</p>
|
||||
<p>Any pattern dealing with personal growth or clearly focus on personal outcome.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">WISDOM</p>
|
||||
<p>Anything worthy of adding to your personal playbook.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-base font-bold text-primary-300 mb-2">OTHER</p>
|
||||
<p>Does not fit anywhere</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import Root from "./input.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
import Input from './Input.svelte';
|
||||
|
||||
export { Input };
|
||||
export default Input;
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils";
|
||||
|
||||
export let value: string = '';
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let id: string | undefined = undefined;
|
||||
export let disabled: boolean = false;
|
||||
export let required: boolean = false;
|
||||
let className: string | undefined = undefined;
|
||||
export let value: string | undefined = undefined;
|
||||
export { className as class };
|
||||
export let readonly = undefined;
|
||||
</script>
|
||||
|
||||
<input
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-lg transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{readonly}
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
on:focus
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousemove
|
||||
on:paste
|
||||
on:input
|
||||
on:wheel|passive
|
||||
{...$$restProps}
|
||||
type="text"
|
||||
{id}
|
||||
bind:value
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{required}
|
||||
class={cn(
|
||||
"block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils";
|
||||
let className = undefined;
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<label
|
||||
class={cn(
|
||||
"p-1 text-sm font-bold leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-200 {className}"
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
<slot />
|
||||
</label>
|
||||
38
web/src/lib/components/ui/modal/Modal.svelte
Normal file
38
web/src/lib/components/ui/modal/Modal.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { fade, scale } from 'svelte/transition';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
}>();
|
||||
|
||||
export let show = false;
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 mt-2"
|
||||
on:click={() => dispatch('close')}
|
||||
transition:fade={{ duration: 200 }}
|
||||
>
|
||||
<div
|
||||
class="relative"
|
||||
on:click|stopPropagation
|
||||
transition:scale={{ duration: 200 }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,8 @@
|
||||
import type { DrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { onMount } from 'svelte';
|
||||
import { noteStore } from '$lib/store/note-store';
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import { clickOutside } from '$lib/actions/clickOutside';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
const toastStore = getToastStore();
|
||||
@@ -82,54 +83,70 @@
|
||||
|
||||
<Drawer width="w-[40%]" class="flex flex-col h-[calc(100vh-theme(spacing.32))] p-4 mt-16">
|
||||
{#if $drawerStore.open}
|
||||
<div class="flex flex-col h-full">
|
||||
<header class="flex-none flex justify-between items-center">
|
||||
<h2 class="m-2 p-1 h2">Notes</h2>
|
||||
<p class="p-2 opacity-70">Notes are saved to <code>`src/lib/content/inbox`</code></p>
|
||||
<p class="p-2 opacity-70">Ctrl + S to save</p>
|
||||
{#if $noteStore.lastSaved}
|
||||
<span class="text-sm opacity-70">
|
||||
Last saved: {$noteStore.lastSaved.toLocaleTimeString()}
|
||||
</span>
|
||||
{/if}
|
||||
<div
|
||||
class="flex flex-col h-full"
|
||||
use:clickOutside={() => {
|
||||
if ($noteStore.isDirty) {
|
||||
if (confirm('You have unsaved changes. Are you sure you want to close?')) {
|
||||
noteStore.reset();
|
||||
drawerStore.close();
|
||||
}
|
||||
} else {
|
||||
drawerStore.close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<header class="flex-none p-2 border-b border-white/10">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold">Notes</h2>
|
||||
{#if $noteStore.lastSaved}
|
||||
<span class="text-xs opacity-70">
|
||||
Last saved: {$noteStore.lastSaved.toLocaleTimeString()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-4 mt-2 text-xs opacity-70">
|
||||
<span>Notes saved to <code>inbox/</code></span>
|
||||
<span>Ctrl + S to save</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-1">
|
||||
<div class="flex-1 p-4 justify-center items-center m-4">
|
||||
|
||||
<div class="flex-1 p-2">
|
||||
<textarea
|
||||
bind:this={textareaEl}
|
||||
bind:value={$noteStore.content}
|
||||
on:input={adjustTextareaHeight}
|
||||
on:keydown={handleKeydown}
|
||||
class="w-full min-h-96 max-h-[500px] overflow-y-auto resize-none p-2 rounded-container-token text-primary-800"
|
||||
placeholder="Enter your text here..."
|
||||
bind:this={textareaEl}
|
||||
value={$noteStore.content}
|
||||
on:input={e => noteStore.updateContent(e.currentTarget.value)}
|
||||
on:keydown={handleKeydown}
|
||||
class="w-full h-full min-h-[300px] resize-none p-2 rounded-lg bg-primary-800/30 border-none text-sm"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="flex-none flex justify-between items-center p-4 mt-auto">
|
||||
<span class="text-sm opacity-70">
|
||||
{#if $noteStore.isDirty}
|
||||
Unsaved changes
|
||||
|
||||
<footer class="flex-none flex justify-between items-center p-2 border-t border-white/10">
|
||||
<span class="text-xs opacity-70">
|
||||
{#if $noteStore.isDirty}
|
||||
Unsaved changes
|
||||
{/if}
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-sm variant-filled-surface"
|
||||
on:click={noteStore.reset}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm variant-filled-primary"
|
||||
on:click={saveContent}
|
||||
>
|
||||
{#if saving}
|
||||
Saving...
|
||||
{:else}
|
||||
Save
|
||||
{/if}
|
||||
</span>
|
||||
<div class="flex gap-2 m-5">
|
||||
<button
|
||||
class="btn p-2 variant-filled-primary"
|
||||
on:click={noteStore.reset}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
class="btn p-2 variant-filled-primary"
|
||||
on:click={saveContent}
|
||||
>
|
||||
{#if saving}
|
||||
Saving...
|
||||
{:else}
|
||||
Save
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
{/if}
|
||||
</Drawer>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
export let value: any = undefined;
|
||||
export let disabled = false;
|
||||
let className = undefined;
|
||||
let className: string | undefined = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{disabled}
|
||||
bind:value
|
||||
class={cn(
|
||||
"select flex h-8 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xs shadow-lg ring-offset-background placeholder:text-muted-primary focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 appearance-none",
|
||||
"select flex h-8 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xs font-medium text-white/90 shadow-lg ring-offset-background placeholder:text-muted-primary focus:outline-none focus:ring-1 focus:ring-ring hover:text-white disabled:cursor-not-allowed disabled:opacity-50 appearance-none transition-colors",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils/utils.ts";
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils";
|
||||
|
||||
let className = undefined;
|
||||
let className: string | undefined = undefined;
|
||||
export let value = 0;
|
||||
export let min = 0;
|
||||
export let max = 100;
|
||||
export let step = 1;
|
||||
export { className as class };
|
||||
|
||||
let sliderEl;
|
||||
let sliderEl: HTMLDivElement;
|
||||
let isDragging = false;
|
||||
|
||||
$: percentage = ((value - min) / (max - min)) * 100;
|
||||
|
||||
function handleMouseDown(e) {
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
isDragging = true;
|
||||
updateValue(e);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
function handleMouseMove(e) {
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!isDragging) return;
|
||||
updateValue(e);
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
function updateValue(e) {
|
||||
function updateValue(e: MouseEvent) {
|
||||
if (!sliderEl) return;
|
||||
const rect = sliderEl.getBoundingClientRect();
|
||||
const pos = (e.clientX - rect.left) / rect.width;
|
||||
@@ -40,8 +40,8 @@
|
||||
value = Math.max(min, Math.min(max, steppedValue));
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
const step = e.shiftKey ? 10 : 1;
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const stepSize = e.shiftKey ? 10 : 1;
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowDown':
|
||||
@@ -76,14 +76,14 @@
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={value}
|
||||
>
|
||||
<div class="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<div class="relative h-0.5 w-full grow overflow-hidden rounded-full bg-white/10">
|
||||
<div
|
||||
class="absolute h-full bg-primary transition-all"
|
||||
class="absolute h-full bg-white/30 transition-all"
|
||||
style="width: {percentage}%"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute h-4 w-4 rounded-full border border-secondary bg-primary-500 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
||||
style="left: calc({percentage}% - 0.5rem)"
|
||||
class="absolute h-2.5 w-2.5 rounded-full bg-white/70 ring-1 ring-white/10 shadow-sm transition-all hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 disabled:pointer-events-none disabled:opacity-50"
|
||||
style="left: calc({percentage}% - 0.3125rem)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { ToastMessage } from '$lib/store/toast-store';
|
||||
|
||||
export let toast: ToastMessage;
|
||||
const TOAST_TIMEOUT = 3000;
|
||||
const TOAST_TIMEOUT = 5000;
|
||||
|
||||
onMount(() => {
|
||||
const timer = setTimeout(() => {
|
||||
|
||||
114
web/src/lib/components/ui/tooltip/Tooltip.svelte
Normal file
114
web/src/lib/components/ui/tooltip/Tooltip.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
export let text: string;
|
||||
export let position: 'top' | 'bottom' | 'left' | 'right' = 'top';
|
||||
|
||||
let tooltipVisible = false;
|
||||
let tooltipElement: HTMLDivElement;
|
||||
|
||||
function showTooltip() {
|
||||
tooltipVisible = true;
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltipVisible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="tooltip-container">
|
||||
<div
|
||||
class="tooltip-trigger"
|
||||
on:mouseenter={showTooltip}
|
||||
on:mouseleave={hideTooltip}
|
||||
on:focusin={showTooltip}
|
||||
on:focusout={hideTooltip}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if tooltipVisible}
|
||||
<div
|
||||
bind:this={tooltipElement}
|
||||
class="tooltip absolute z-[9999] px-2 py-1 text-xs rounded bg-gray-900/90 text-white whitespace-nowrap shadow-lg backdrop-blur-sm"
|
||||
class:top="{position === 'top'}"
|
||||
class:bottom="{position === 'bottom'}"
|
||||
class:left="{position === 'left'}"
|
||||
class:right="{position === 'right'}"
|
||||
>
|
||||
{text}
|
||||
<div class="tooltip-arrow" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tooltip-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
pointer-events: none;
|
||||
transition: all 150ms ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip.top {
|
||||
bottom: calc(100% + 5px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip.bottom {
|
||||
top: calc(100% + 5px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip.left {
|
||||
right: calc(100% + 5px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tooltip.right {
|
||||
left: calc(100% + 5px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: inherit;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.tooltip.top .tooltip-arrow {
|
||||
bottom: -4px;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.tooltip.bottom .tooltip-arrow {
|
||||
top: -4px;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.tooltip.left .tooltip-arrow {
|
||||
right: -4px;
|
||||
top: 50%;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.tooltip.right .tooltip-arrow {
|
||||
left: -4px;
|
||||
top: 50%;
|
||||
margin-top: -4px;
|
||||
}
|
||||
</style>
|
||||
16
web/src/lib/config/features.ts
Normal file
16
web/src/lib/config/features.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
interface FeatureFlags {
|
||||
enableObsidianIntegration: boolean;
|
||||
}
|
||||
|
||||
export const featureFlags = writable<FeatureFlags>({
|
||||
enableObsidianIntegration: true // Set to true for development
|
||||
});
|
||||
|
||||
export function toggleObsidianIntegration(enabled: boolean) {
|
||||
featureFlags.update(flags => ({
|
||||
...flags,
|
||||
enableObsidianIntegration: enabled
|
||||
}));
|
||||
}
|
||||
16
web/src/lib/interfaces/LanguageDisplay.svelte
Normal file
16
web/src/lib/interfaces/LanguageDisplay.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
</script>
|
||||
|
||||
<div class="language-indicator">
|
||||
Current Language: {$languageStore}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.language-indicator {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
export type MessageRole = 'system' | 'user' | 'assistant';
|
||||
export type ResponseFormat = 'markdown' | 'mermaid' | 'plain';
|
||||
export type ResponseFormat = 'markdown' | 'mermaid' | 'plain' | 'loading';
|
||||
export type ResponseType = 'content' | 'error' | 'complete';
|
||||
|
||||
export interface ChatPrompt {
|
||||
@@ -28,6 +28,7 @@ export interface ChatRequest {
|
||||
export interface Message {
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
format?: ResponseFormat;
|
||||
}
|
||||
|
||||
export interface ChatState {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
export interface Pattern {
|
||||
Name: string;
|
||||
Description: string;
|
||||
Pattern: string; // | object
|
||||
import type { StorageEntity } from './storage-interface';
|
||||
|
||||
// Interface matching the JSON structure from pattern_descriptions.json
|
||||
export interface PatternDescription {
|
||||
patternName: string;
|
||||
description: string;
|
||||
tags?: string[]; // Optional tags property for PatternDescription
|
||||
}
|
||||
|
||||
// Interface for storage compatibility - must use uppercase for StorageEntity
|
||||
export interface Pattern extends StorageEntity {
|
||||
Name: string; // maps to patternName from JSON
|
||||
Description: string; // maps to description from JSON
|
||||
Pattern: string; // pattern content from API
|
||||
tags: string[]; // array of tag strings
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
import type {
|
||||
ChatRequest,
|
||||
StreamResponse,
|
||||
import type {
|
||||
ChatRequest,
|
||||
StreamResponse,
|
||||
ChatError as IChatError,
|
||||
ChatPrompt
|
||||
} from '$lib/interfaces/chat-interface';
|
||||
import { get } from 'svelte/store';
|
||||
import { modelConfig } from '$lib/store/model-store';
|
||||
import { systemPrompt } from '$lib/store/pattern-store';
|
||||
import { systemPrompt, selectedPatternName } from '$lib/store/pattern-store';
|
||||
import { chatConfig } from '$lib/store/chat-config';
|
||||
import { messageStore } from '$lib/store/chat-store'; // Import messageStore
|
||||
import { messageStore } from '$lib/store/chat-store';
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
|
||||
class LanguageValidator {
|
||||
constructor(private targetLanguage: string) {}
|
||||
|
||||
enforceLanguage(content: string): string {
|
||||
if (this.targetLanguage === 'en') return content;
|
||||
return `[Language: ${this.targetLanguage}]\n${content}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatError extends Error implements IChatError {
|
||||
constructor(
|
||||
@@ -22,22 +32,30 @@ export class ChatError extends Error implements IChatError {
|
||||
}
|
||||
|
||||
export class ChatService {
|
||||
private validator: LanguageValidator;
|
||||
|
||||
constructor() {
|
||||
this.validator = new LanguageValidator(get(languageStore));
|
||||
}
|
||||
|
||||
private async fetchStream(request: ChatRequest): Promise<ReadableStream<StreamResponse>> {
|
||||
try {
|
||||
console.log('\n=== ChatService Request Start ===');
|
||||
console.log('1. Request details:', {
|
||||
language: get(languageStore),
|
||||
pattern: get(selectedPatternName),
|
||||
promptCount: request.prompts?.length,
|
||||
messageCount: request.messages?.length
|
||||
});
|
||||
|
||||
const response = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ChatError(
|
||||
`HTTP error! status: ${response.status}`,
|
||||
'HTTP_ERROR',
|
||||
{ status: response.status }
|
||||
);
|
||||
throw new ChatError(`HTTP error! status: ${response.status}`, 'HTTP_ERROR', { status: response.status });
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
@@ -47,82 +65,140 @@ export class ChatService {
|
||||
|
||||
return this.createMessageStream(reader);
|
||||
} catch (error) {
|
||||
if (error instanceof ChatError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ChatError(
|
||||
'Failed to fetch chat stream',
|
||||
'FETCH_ERROR',
|
||||
error
|
||||
);
|
||||
if (error instanceof ChatError) throw error;
|
||||
throw new ChatError('Failed to fetch chat stream', 'FETCH_ERROR', error);
|
||||
}
|
||||
}
|
||||
|
||||
private createMessageStream(reader: ReadableStreamDefaultReader<Uint8Array>): ReadableStream<StreamResponse> {
|
||||
let buffer = '';
|
||||
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += new TextDecoder().decode(value);
|
||||
const messages = buffer
|
||||
.split('\n\n')
|
||||
.filter(msg => msg.startsWith('data: '));
|
||||
|
||||
if (messages.length > 1) {
|
||||
buffer = messages.pop() || '';
|
||||
|
||||
for (const msg of messages) {
|
||||
controller.enqueue(JSON.parse(msg.slice(6)) as StreamResponse);
|
||||
private cleanPatternOutput(content: string): string {
|
||||
// Remove markdown fence if present
|
||||
content = content.replace(/^```markdown\n/, '');
|
||||
content = content.replace(/\n```$/, '');
|
||||
|
||||
// Existing cleaning
|
||||
content = content.replace(/^# OUTPUT\s*\n/, '');
|
||||
content = content.replace(/^\s*\n/, '');
|
||||
content = content.replace(/\n\s*$/, '');
|
||||
content = content.replace(/^#\s+([A-Z]+):/gm, '$1:');
|
||||
content = content.replace(/^#\s+([A-Z]+)\s*$/gm, '$1');
|
||||
content = content.trim();
|
||||
content = content.replace(/\n{3,}/g, '\n\n');
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private createMessageStream(reader: ReadableStreamDefaultReader<Uint8Array>): ReadableStream<StreamResponse> {
|
||||
let buffer = '';
|
||||
const cleanPatternOutput = this.cleanPatternOutput.bind(this);
|
||||
const language = get(languageStore);
|
||||
const validator = new LanguageValidator(language);
|
||||
|
||||
const processResponse = (response: StreamResponse) => {
|
||||
const pattern = get(selectedPatternName);
|
||||
|
||||
if (pattern) {
|
||||
response.content = cleanPatternOutput(response.content);
|
||||
// Simplified format determination - always markdown unless mermaid
|
||||
const isMermaid = [
|
||||
'graph TD', 'gantt', 'flowchart',
|
||||
'sequenceDiagram', 'classDiagram', 'stateDiagram'
|
||||
].some(starter => response.content.trim().startsWith(starter));
|
||||
|
||||
response.format = isMermaid ? 'mermaid' : 'markdown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (response.type === 'content') {
|
||||
response.content = validator.enforceLanguage(response.content);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
if (buffer.startsWith('data: ')) {
|
||||
controller.enqueue(JSON.parse(buffer.slice(6)) as StreamResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(new ChatError(
|
||||
'Error processing stream',
|
||||
'STREAM_PROCESSING_ERROR',
|
||||
error
|
||||
));
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
buffer += new TextDecoder().decode(value);
|
||||
const messages = buffer.split('\n\n').filter(msg => msg.startsWith('data: '));
|
||||
|
||||
cancel() {
|
||||
reader.cancel();
|
||||
}
|
||||
});
|
||||
if (messages.length > 1) {
|
||||
buffer = messages.pop() || '';
|
||||
for (const msg of messages) {
|
||||
try {
|
||||
let response = JSON.parse(msg.slice(6)) as StreamResponse;
|
||||
response = processResponse(response);
|
||||
controller.enqueue(response);
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing stream message:', parseError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.startsWith('data: ')) {
|
||||
try {
|
||||
let response = JSON.parse(buffer.slice(6)) as StreamResponse;
|
||||
response = processResponse(response);
|
||||
controller.enqueue(response);
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing final message:', parseError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(new ChatError('Stream processing error', 'STREAM_ERROR', error));
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
reader.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createChatPrompt(userInput: string, systemPromptText?: string): ChatPrompt {
|
||||
const config = get(modelConfig);
|
||||
const language = get(languageStore);
|
||||
|
||||
const languageInstruction = language !== 'en'
|
||||
? `You MUST respond in ${language} language. ALL output, including section headers, titles, and formatting, MUST be translated into ${language}. It is CRITICAL that you translate ALL headers, such as SUMMARY, IDEAS, QUOTES, TAKEAWAYS, MAIN POINTS, etc., into ${language}. Maintain markdown formatting in the response. Do not output any English headers.`
|
||||
: '';
|
||||
|
||||
const finalSystemPrompt = languageInstruction + (systemPromptText ?? get(systemPrompt));
|
||||
console.log('Final system prompt in createChatPrompt:', finalSystemPrompt);
|
||||
const finalUserInput = language !== 'en'
|
||||
? `${userInput}\n\nIMPORTANT: Respond in ${language} language only.`
|
||||
: userInput;
|
||||
|
||||
return {
|
||||
userInput,
|
||||
systemPrompt: systemPromptText ?? get(systemPrompt),
|
||||
model: config.model,
|
||||
patternName: ''
|
||||
userInput: finalUserInput,
|
||||
systemPrompt: finalSystemPrompt,
|
||||
model: config.model,
|
||||
patternName: get(selectedPatternName)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public async createChatRequest(userInput: string, systemPromptText?: string, isPattern: boolean = false): Promise<ChatRequest> {
|
||||
const prompt = this.createChatPrompt(userInput, systemPromptText);
|
||||
const config = get(chatConfig);
|
||||
|
||||
return {
|
||||
prompts: [prompt],
|
||||
messages: [],
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
public async createChatRequest(userInput: string, systemPromptText?: string): Promise<ChatRequest> {
|
||||
const prompt = this.createChatPrompt(userInput, systemPromptText);
|
||||
const config = get(chatConfig);
|
||||
const messages = get(messageStore);
|
||||
|
||||
return {
|
||||
prompts: [prompt],
|
||||
messages: messages,
|
||||
...config
|
||||
};
|
||||
public async streamPattern(userInput: string, systemPromptText?: string): Promise<ReadableStream<StreamResponse>> {
|
||||
const request = await this.createChatRequest(userInput, systemPromptText, true);
|
||||
return this.fetchStream(request);
|
||||
}
|
||||
|
||||
public async streamChat(userInput: string, systemPromptText?: string): Promise<ReadableStream<StreamResponse>> {
|
||||
@@ -132,37 +208,33 @@ export class ChatService {
|
||||
|
||||
public async processStream(
|
||||
stream: ReadableStream<StreamResponse>,
|
||||
onContent: (content: string) => void,
|
||||
onContent: (content: string, response?: StreamResponse) => void,
|
||||
onError: (error: Error) => void
|
||||
): Promise<void> {
|
||||
const reader = stream.getReader();
|
||||
let accumulatedContent = '';
|
||||
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
|
||||
if (value.type === 'error') {
|
||||
throw new ChatError(value.content, 'STREAM_CONTENT_ERROR');
|
||||
}
|
||||
|
||||
accumulatedContent += value.content;
|
||||
onContent(accumulatedContent);
|
||||
|
||||
if (value.type === 'content') {
|
||||
onContent(value.content, value);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ChatError) {
|
||||
onError(error);
|
||||
} else {
|
||||
onError(new ChatError(
|
||||
'Error processing stream content',
|
||||
'STREAM_PROCESSING_ERROR',
|
||||
error
|
||||
));
|
||||
}
|
||||
onError(error instanceof ChatError ? error : new ChatError('Stream processing error', 'STREAM_ERROR', error));
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,38 +1,80 @@
|
||||
import { YoutubeTranscript } from 'youtube-transcript';
|
||||
import { get } from 'svelte/store';
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
|
||||
export interface TranscriptResponse {
|
||||
transcript: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(text: string): string {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.innerHTML = text;
|
||||
return textarea.value;
|
||||
}
|
||||
|
||||
export async function getTranscript(url: string): Promise<TranscriptResponse> {
|
||||
try {
|
||||
const videoId = extractVideoId(url);
|
||||
if (!videoId) {
|
||||
throw new Error('Invalid YouTube URL');
|
||||
const originalLanguage = get(languageStore);
|
||||
console.log('\n=== YouTube Transcript Service Start ===');
|
||||
console.log('1. Request details:', {
|
||||
url,
|
||||
endpoint: '/chat',
|
||||
method: 'POST',
|
||||
isYouTubeURL: url.includes('youtube.com') || url.includes('youtu.be'),
|
||||
originalLanguage
|
||||
});
|
||||
|
||||
const response = await fetch('/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
language: originalLanguage // Pass original language to server
|
||||
})
|
||||
});
|
||||
|
||||
console.log('2. Server response:', {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
type: response.type,
|
||||
originalLanguage,
|
||||
currentLanguage: get(languageStore)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId);
|
||||
const transcript = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join(' ');
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
const transcriptTitle = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join('');
|
||||
// Decode HTML entities in transcript
|
||||
data.transcript = decodeHtmlEntities(data.transcript);
|
||||
|
||||
// TODO: Add title fetching
|
||||
return {
|
||||
transcript,
|
||||
title: videoId // Just returning the video ID as title
|
||||
};
|
||||
// Ensure language is preserved
|
||||
if (get(languageStore) !== originalLanguage) {
|
||||
console.log('3a. Restoring original language:', originalLanguage);
|
||||
languageStore.set(originalLanguage);
|
||||
}
|
||||
|
||||
console.log('3b. Processed transcript:', {
|
||||
status: response.status,
|
||||
transcriptLength: data.transcript.length,
|
||||
firstChars: data.transcript.substring(0, 100),
|
||||
hasError: !!data.error,
|
||||
videoId: data.title,
|
||||
originalLanguage,
|
||||
currentLanguage: get(languageStore)
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Transcript fetch error:', error);
|
||||
throw new Error('Failed to fetch transcript');
|
||||
throw error instanceof Error ? error : new Error('Failed to fetch transcript');
|
||||
}
|
||||
}
|
||||
|
||||
function extractVideoId(url: string): string | null {
|
||||
const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { writable, derived, get } from 'svelte/store';
|
||||
import type { ChatState, Message } from '$lib/interfaces/chat-interface';
|
||||
import type { ChatState, Message, StreamResponse } from '$lib/interfaces/chat-interface';
|
||||
import { ChatService, ChatError } from '$lib/services/ChatService';
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
import { selectedPatternName } from '$lib/store/pattern-store';
|
||||
|
||||
// Initialize chat service
|
||||
const chatService = new ChatService();
|
||||
@@ -67,54 +69,86 @@ export const revertLastMessage = () => {
|
||||
messageStore.update(messages => messages.slice(0, -1));
|
||||
};
|
||||
|
||||
export async function sendMessage(userInput: string, systemPromptText?: string) {
|
||||
try {
|
||||
const $streaming = get(streamingStore);
|
||||
if ($streaming) {
|
||||
throw new ChatError('Message submission blocked - already streaming', 'STREAMING_BLOCKED');
|
||||
}
|
||||
|
||||
streamingStore.set(true);
|
||||
errorStore.set(null);
|
||||
|
||||
// Add user message
|
||||
messageStore.update(messages => [...messages, { role: 'user', content: userInput }]);
|
||||
|
||||
const stream = await chatService.streamChat(userInput, systemPromptText);
|
||||
|
||||
await chatService.processStream(
|
||||
stream,
|
||||
(content) => {
|
||||
messageStore.update(messages => {
|
||||
const newMessages = [...messages];
|
||||
const lastMessage = newMessages[newMessages.length - 1];
|
||||
|
||||
if (lastMessage?.role === 'assistant') {
|
||||
lastMessage.content = content;
|
||||
} else {
|
||||
newMessages.push({
|
||||
role: 'assistant',
|
||||
content
|
||||
});
|
||||
}
|
||||
|
||||
return newMessages;
|
||||
export async function sendMessage(content: string, systemPromptText?: string, isSystem: boolean = false) {
|
||||
try {
|
||||
console.log('\n=== Message Processing Start ===');
|
||||
console.log('1. Initial state:', {
|
||||
isSystem,
|
||||
hasSystemPrompt: !!systemPromptText,
|
||||
currentLanguage: get(languageStore),
|
||||
pattern: get(selectedPatternName)
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
handleError(error);
|
||||
}
|
||||
);
|
||||
|
||||
streamingStore.set(false);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
handleError(error);
|
||||
} else {
|
||||
handleError(String(error));
|
||||
const $streaming = get(streamingStore);
|
||||
if ($streaming) {
|
||||
throw new ChatError('Message submission blocked - already streaming', 'STREAMING_BLOCKED');
|
||||
}
|
||||
|
||||
streamingStore.set(true);
|
||||
errorStore.set(null);
|
||||
|
||||
// Add message
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: isSystem ? 'system' : 'user',
|
||||
content
|
||||
}]);
|
||||
|
||||
console.log('2. Message added:', {
|
||||
role: isSystem ? 'system' : 'user',
|
||||
language: get(languageStore)
|
||||
});
|
||||
|
||||
if (!isSystem) {
|
||||
console.log('3. Preparing chat stream:', {
|
||||
language: get(languageStore),
|
||||
pattern: get(selectedPatternName),
|
||||
hasSystemPrompt: !!systemPromptText
|
||||
});
|
||||
|
||||
const stream = await chatService.streamChat(content, systemPromptText);
|
||||
console.log('4. Stream created');
|
||||
|
||||
await chatService.processStream(
|
||||
stream,
|
||||
(content: string, response?: StreamResponse) => {
|
||||
messageStore.update(messages => {
|
||||
const newMessages = [...messages];
|
||||
const lastMessage = newMessages[newMessages.length - 1];
|
||||
|
||||
if (lastMessage?.role === 'assistant') {
|
||||
lastMessage.content = content;
|
||||
lastMessage.format = response?.format;
|
||||
console.log('Message updated:', {
|
||||
role: 'assistant',
|
||||
format: lastMessage.format
|
||||
});
|
||||
} else {
|
||||
newMessages.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
format: response?.format
|
||||
});
|
||||
}
|
||||
|
||||
return newMessages;
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
handleError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
streamingStore.set(false);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
handleError(error);
|
||||
} else {
|
||||
handleError(String(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export types for convenience
|
||||
|
||||
36
web/src/lib/store/favorites-store.ts
Normal file
36
web/src/lib/store/favorites-store.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// Load favorites from localStorage if available
|
||||
const storedFavorites = typeof localStorage !== 'undefined'
|
||||
? JSON.parse(localStorage.getItem('favoritePatterns') || '[]')
|
||||
: [];
|
||||
|
||||
const createFavoritesStore = () => {
|
||||
const { subscribe, set, update } = writable<string[]>(storedFavorites);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
toggleFavorite: (patternName: string) => {
|
||||
update(favorites => {
|
||||
const newFavorites = favorites.includes(patternName)
|
||||
? favorites.filter(name => name !== patternName)
|
||||
: [...favorites, patternName];
|
||||
|
||||
// Save to localStorage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('favoritePatterns', JSON.stringify(newFavorites));
|
||||
}
|
||||
|
||||
return newFavorites;
|
||||
});
|
||||
},
|
||||
reset: () => {
|
||||
set([]);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem('favoritePatterns');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const favorites = createFavoritesStore();
|
||||
13
web/src/lib/store/language-store.ts
Normal file
13
web/src/lib/store/language-store.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const storedLanguage = browser ? localStorage.getItem('selectedLanguage') || 'en' : 'en';
|
||||
const languageStore = writable<string>(storedLanguage);
|
||||
|
||||
if (browser) {
|
||||
languageStore.subscribe(value => {
|
||||
localStorage.setItem('selectedLanguage', value);
|
||||
});
|
||||
}
|
||||
|
||||
export { languageStore };
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writable} from 'svelte/store';
|
||||
import { writable } from 'svelte/store';
|
||||
import { modelsApi } from '$lib/api/models';
|
||||
import { configApi } from '$lib/api/config';
|
||||
import type { VendorModel, ModelConfig } from '$lib/interfaces/model-interface';
|
||||
|
||||
@@ -10,105 +10,99 @@ interface NoteState {
|
||||
|
||||
function createNoteStore() {
|
||||
const { subscribe, set, update } = writable<NoteState>({
|
||||
content: '',
|
||||
lastSaved: null,
|
||||
isDirty: false
|
||||
content: '',
|
||||
lastSaved: null,
|
||||
isDirty: false
|
||||
});
|
||||
|
||||
const createFrontmatter = (content: string): Frontmatter => {
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString();
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString();
|
||||
const title = `Note ${now.toLocaleString()}`;
|
||||
const cleanContent = content
|
||||
.replace(/[#*`_]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// Generate a timestamp-based title instead of using content
|
||||
const title = `Note ${now.toLocaleString()}`;
|
||||
|
||||
// Clean up content for description - remove markdown and extra whitespace
|
||||
const cleanContent = content
|
||||
.replace(/[#*`_]/g, '') // Remove markdown characters
|
||||
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||
.trim();
|
||||
|
||||
return {
|
||||
title,
|
||||
aliases: [''],
|
||||
description: cleanContent.slice(0, 150) + (cleanContent.length > 150 ? '...' : ''),
|
||||
date: dateStr,
|
||||
tags: ['inbox', 'note'],
|
||||
updated: dateStr,
|
||||
author: 'User',
|
||||
};
|
||||
return {
|
||||
title,
|
||||
aliases: [''],
|
||||
description: cleanContent.slice(0, 150) + (cleanContent.length > 150 ? '...' : ''),
|
||||
date: dateStr,
|
||||
tags: ['inbox', 'note'],
|
||||
updated: dateStr,
|
||||
author: 'User',
|
||||
};
|
||||
};
|
||||
|
||||
const generateUniqueFilename = () => {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().split('T')[0];
|
||||
const time = now.toISOString().split('T')[1]
|
||||
.replace(/:/g, '-')
|
||||
.split('.')[0];
|
||||
return `${date}-${time}.md`;
|
||||
const now = new Date();
|
||||
const date = now.toISOString().split('T')[0];
|
||||
const time = now.toISOString().split('T')[1]
|
||||
.replace(/:/g, '-')
|
||||
.split('.')[0];
|
||||
return `${date}-${time}.md`;
|
||||
};
|
||||
|
||||
const saveToFile = async (content: string) => {
|
||||
if (!browser) return;
|
||||
if (!browser) return;
|
||||
|
||||
const filename = generateUniqueFilename();
|
||||
const frontmatter = createFrontmatter(content);
|
||||
|
||||
// Format frontmatter without extra indentation
|
||||
const fileContent = `---
|
||||
const filename = generateUniqueFilename();
|
||||
const frontmatter = createFrontmatter(content);
|
||||
const fileContent = `---
|
||||
title: ${frontmatter.title}
|
||||
aliases: [${frontmatter.aliases.map(aliases => `"${aliases}"`).join(', ')}]
|
||||
aliases: [${(frontmatter.aliases || []).map(alias => `"${alias}"`).join(', ')}]
|
||||
description: ${frontmatter.description}
|
||||
date: ${frontmatter.date}
|
||||
tags: [${frontmatter.tags.map(tag => `"${tag}"`).join(', ')}]
|
||||
tags: [${(frontmatter.tags || []).map(tag => `"${tag}"`).join(', ')}]
|
||||
updated: ${frontmatter.updated}
|
||||
author: ${frontmatter.author}
|
||||
---
|
||||
|
||||
${content}`; // Original content preserved as-is
|
||||
${content}`;
|
||||
|
||||
const response = await fetch('/notes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename,
|
||||
content: fileContent
|
||||
})
|
||||
});
|
||||
const response = await fetch('/notes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename,
|
||||
content: fileContent
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
return filename;
|
||||
return filename;
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
updateContent: (content: string) => update(state => ({
|
||||
...state,
|
||||
content,
|
||||
isDirty: true
|
||||
})),
|
||||
save: async () => {
|
||||
const state = get({ subscribe });
|
||||
const filename = await saveToFile(state.content);
|
||||
subscribe,
|
||||
updateContent: (content: string) => update(state => ({
|
||||
...state,
|
||||
content,
|
||||
isDirty: true
|
||||
})),
|
||||
save: async () => {
|
||||
const state = get({ subscribe });
|
||||
const filename = await saveToFile(state.content);
|
||||
|
||||
update(state => ({
|
||||
...state,
|
||||
lastSaved: new Date(),
|
||||
isDirty: false
|
||||
}));
|
||||
update(state => ({
|
||||
...state,
|
||||
lastSaved: new Date(),
|
||||
isDirty: false
|
||||
}));
|
||||
|
||||
return filename;
|
||||
},
|
||||
reset: () => set({
|
||||
content: '',
|
||||
lastSaved: null,
|
||||
isDirty: false
|
||||
})
|
||||
return filename;
|
||||
},
|
||||
reset: () => set({
|
||||
content: '',
|
||||
lastSaved: null,
|
||||
isDirty: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
68
web/src/lib/store/obsidian-store.ts
Normal file
68
web/src/lib/store/obsidian-store.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { writable, get } from 'svelte/store';
|
||||
import { featureFlags } from '../config/features';
|
||||
|
||||
export interface ObsidianSettings {
|
||||
saveToObsidian: boolean;
|
||||
noteName: string;
|
||||
}
|
||||
|
||||
// Keep existing defaultSettings
|
||||
const defaultSettings: ObsidianSettings = {
|
||||
saveToObsidian: false,
|
||||
noteName: ''
|
||||
};
|
||||
|
||||
// Keep existing store initialization
|
||||
export const obsidianSettings = writable<ObsidianSettings>(defaultSettings);
|
||||
|
||||
// Add notification store
|
||||
export const saveNotification = writable<string>('');
|
||||
|
||||
// Keep existing update function with notification enhancement
|
||||
export function updateObsidianSettings(settings: Partial<ObsidianSettings>) {
|
||||
const enabled = get(featureFlags).enableObsidianIntegration;
|
||||
console.log('Updating Obsidian settings:', settings, 'Integration enabled:', enabled);
|
||||
|
||||
if (!enabled) {
|
||||
console.log('Obsidian integration disabled, not updating settings');
|
||||
return;
|
||||
}
|
||||
|
||||
obsidianSettings.update(current => {
|
||||
const updated = {
|
||||
...current,
|
||||
...settings
|
||||
};
|
||||
|
||||
// Add notification after successful save
|
||||
if (settings.saveToObsidian === false && current.noteName) {
|
||||
saveNotification.set('Note saved to Obsidian!');
|
||||
setTimeout(() => saveNotification.set(''), 3000);
|
||||
}
|
||||
|
||||
console.log('Updated Obsidian settings:', updated);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
// Reset settings to default
|
||||
export function resetObsidianSettings() {
|
||||
const enabled = get(featureFlags).enableObsidianIntegration;
|
||||
if (!enabled) return;
|
||||
|
||||
obsidianSettings.set(defaultSettings);
|
||||
}
|
||||
|
||||
// Helper to get file path
|
||||
export function getObsidianFilePath(noteName: string): string | undefined {
|
||||
const enabled = get(featureFlags).enableObsidianIntegration;
|
||||
if (!enabled || !noteName) return undefined;
|
||||
|
||||
return `myfiles/Fabric_obsidian/${
|
||||
new Date().toISOString().split('T')[0]
|
||||
}-${noteName.trim()}.md`;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import { createStorageAPI } from '$lib/api/base';
|
||||
import type { Pattern } from '$lib/interfaces/pattern-interface';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { Pattern, PatternDescription } from '$lib/interfaces/pattern-interface';
|
||||
import { get, writable, derived } from 'svelte/store';
|
||||
import { languageStore } from './language-store';
|
||||
|
||||
// Store for all patterns
|
||||
const allPatterns = writable<Pattern[]>([]);
|
||||
|
||||
// Filtered patterns based on language
|
||||
export const patterns = derived(
|
||||
[allPatterns, languageStore],
|
||||
([$allPatterns, $language]) => {
|
||||
if (!$language) return $allPatterns;
|
||||
// If language is selected, filter out patterns of other languages
|
||||
return $allPatterns.filter(p => {
|
||||
// Keep all patterns if no language is selected
|
||||
if (!$language) return true;
|
||||
|
||||
// Check if pattern has a language prefix (e.g., en_, fr_)
|
||||
const match = p.Name.match(/^([a-z]{2})_/);
|
||||
if (!match) return true; // Keep patterns without language prefix
|
||||
|
||||
// Only filter out patterns that have a different language prefix
|
||||
const patternLang = match[1];
|
||||
return patternLang === $language;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const patterns = writable<Pattern[]>([]);
|
||||
export const systemPrompt = writable<string>('');
|
||||
export const selectedPatternName = writable<string>('');
|
||||
|
||||
export const setSystemPrompt = (prompt: string) => {
|
||||
console.log('Setting system prompt:', prompt);
|
||||
@@ -16,26 +41,47 @@ export const patternAPI = {
|
||||
|
||||
async loadPatterns() {
|
||||
try {
|
||||
// First load pattern descriptions
|
||||
const descriptionsResponse = await fetch('/data/pattern_descriptions.json');
|
||||
const descriptionsData = await descriptionsResponse.json();
|
||||
const descriptions = descriptionsData.patterns as PatternDescription[];
|
||||
console.log("Loaded pattern descriptions:", descriptions.length);
|
||||
|
||||
// Then load pattern names and contents
|
||||
const response = await fetch(`/api/patterns/names`);
|
||||
const data = await response.json();
|
||||
console.log("Load Patterns:", data);
|
||||
console.log("Loading patterns from API...");
|
||||
|
||||
// Create an array of promises to fetch all pattern contents
|
||||
const patternsPromises = data.map(async (pattern: string) => {
|
||||
try {
|
||||
console.log(`Loading pattern: ${pattern}`);
|
||||
const patternResponse = await fetch(`/api/patterns/${pattern}`);
|
||||
const patternData = await patternResponse.json();
|
||||
console.log(`Pattern ${pattern} content length:`, patternData.Pattern?.length || 0);
|
||||
|
||||
// Find matching description from JSON
|
||||
const desc = descriptions.find(d => d.patternName === pattern);
|
||||
if (!desc) {
|
||||
console.warn(`No description found for pattern: ${pattern}`);
|
||||
}
|
||||
|
||||
return {
|
||||
Name: pattern,
|
||||
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: patternData.Pattern
|
||||
Description: desc?.description || pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: patternData.Pattern || "",
|
||||
tags: desc?.tags || [] // Add tags from description
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to load pattern ${pattern}:`, error);
|
||||
// Still try to get description even if pattern content fails
|
||||
const desc = descriptions.find(d => d.patternName === pattern);
|
||||
return {
|
||||
Name: pattern,
|
||||
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: ""
|
||||
Description: desc?.description || pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: "",
|
||||
tags: desc?.tags || [] // Add tags here too for consistency
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -43,25 +89,32 @@ export const patternAPI = {
|
||||
// Wait for all pattern contents to be fetched
|
||||
const loadedPatterns = await Promise.all(patternsPromises);
|
||||
console.log("Patterns with content:", loadedPatterns);
|
||||
patterns.set(loadedPatterns);
|
||||
allPatterns.set(loadedPatterns);
|
||||
return loadedPatterns;
|
||||
} catch (error) {
|
||||
console.error('Failed to load patterns:', error);
|
||||
patterns.set([]);
|
||||
allPatterns.set([]);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
selectPattern(patternName: string) {
|
||||
const allPatterns = get(patterns);
|
||||
const patterns = get(allPatterns);
|
||||
console.log('Selecting pattern:', patternName);
|
||||
const selectedPattern = allPatterns.find(p => p.Name === patternName);
|
||||
const selectedPattern = patterns.find(p => p.Name === patternName);
|
||||
if (selectedPattern) {
|
||||
console.log('Found pattern content:', selectedPattern.Pattern);
|
||||
setSystemPrompt(selectedPattern.Pattern.trim());
|
||||
console.log('Found pattern content (length: ' + selectedPattern.Pattern.length + '):', selectedPattern.Pattern);
|
||||
// Log the first and last 100 characters to verify content
|
||||
console.log('First 100 chars:', selectedPattern.Pattern.substring(0, 100));
|
||||
console.log('Last 100 chars:', selectedPattern.Pattern.substring(selectedPattern.Pattern.length - 100));
|
||||
console.log(`Setting system prompt with content length: ${selectedPattern.Pattern.length}`);
|
||||
console.log(`Content preview:`, selectedPattern.Pattern.substring(0, 100));
|
||||
setSystemPrompt(selectedPattern.Pattern);
|
||||
selectedPatternName.set(patternName); // Make sure this is set before setting system prompt
|
||||
} else {
|
||||
console.log('No pattern found for name:', patternName);
|
||||
setSystemPrompt('');
|
||||
selectedPatternName.set('');
|
||||
}
|
||||
console.log('System prompt store value after setting:', get(systemPrompt));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ export const sessionAPI = {
|
||||
const response = await fetch(`/api/sessions/names`);
|
||||
const sessionNames: string[] = await response.json();
|
||||
|
||||
// Add null check and default to empty array
|
||||
if (!sessionNames) {
|
||||
sessions.set([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
const sessionPromises = sessionNames.map(async (name: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/${name}`);
|
||||
@@ -44,6 +50,7 @@ export const sessionAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
selectSession(sessionName: string) {
|
||||
const allSessions = get(sessions);
|
||||
const selectedSession = allSessions.find(session => session.Name === sessionName);
|
||||
|
||||
4
web/src/lib/types/index.ts
Normal file
4
web/src/lib/types/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Pattern {
|
||||
patternName: string;
|
||||
description: string;
|
||||
}
|
||||
@@ -1,34 +1,24 @@
|
||||
<script>
|
||||
import '../app.postcss';
|
||||
import { AppShell } from '@skeletonlabs/skeleton';
|
||||
import { Toast } from '@skeletonlabs/skeleton';
|
||||
import ToastContainer from '$lib/components/ui/toast/ToastContainer.svelte';
|
||||
import ToastContainer from '$lib/components/ui/toast/ToastContainer.svelte';
|
||||
import Footer from '$lib/components/home/Footer.svelte';
|
||||
import Header from '$lib/components/home/Header.svelte';
|
||||
import { initializeStores } from '@skeletonlabs/skeleton';
|
||||
import { initializeStores, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { page } from '$app/stores';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { onMount } from 'svelte';
|
||||
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { toastStore } from '$lib/store/toast-store';
|
||||
|
||||
// Initialize stores
|
||||
initializeStores();
|
||||
const drawerStore = getDrawerStore();
|
||||
const toastStore = getToastStore();
|
||||
|
||||
onMount(() => {
|
||||
toastStore.trigger({
|
||||
type: 'info',
|
||||
message: "👋 Welcome to the site! Tell people about yourself and what you do.",
|
||||
background: 'variant-filled-primary',
|
||||
timeout: 3333,
|
||||
hoverable: true
|
||||
});
|
||||
toastStore.info("👋 Welcome to the site! Tell people about yourself and what you do.");
|
||||
});
|
||||
</script>
|
||||
|
||||
<Toast />
|
||||
<ToastContainer />
|
||||
|
||||
{#key $page.url.pathname}
|
||||
|
||||
46
web/src/routes/api/youtube/transcript/+server.ts
Normal file
46
web/src/routes/api/youtube/transcript/+server.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { YoutubeTranscript } from 'youtube-transcript';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
console.log('Received request body:', body);
|
||||
|
||||
const { url } = body;
|
||||
if (!url) {
|
||||
return json({ error: 'URL is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('Fetching transcript for URL:', url);
|
||||
|
||||
// Extract video ID
|
||||
const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
|
||||
const videoId = match ? match[1] : null;
|
||||
|
||||
if (!videoId) {
|
||||
return json({ error: 'Invalid YouTube URL' }, { status: 400 });
|
||||
}
|
||||
|
||||
const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId);
|
||||
const transcript = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join(' ');
|
||||
|
||||
const response = {
|
||||
transcript,
|
||||
title: videoId
|
||||
};
|
||||
|
||||
console.log('Successfully fetched transcript, preparing response');
|
||||
console.log('Response (first 200 chars):', transcript.slice(0, 200) + '...');
|
||||
|
||||
return json(response);
|
||||
} catch (error) {
|
||||
console.error('Server error:', error);
|
||||
return json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to fetch transcript' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -39,4 +39,4 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,33 +1,162 @@
|
||||
// For the Youtube API
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getTranscript } from '$lib/services/transcriptService';
|
||||
import { YoutubeTranscript } from 'youtube-transcript';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
console.log('Received request body:', body);
|
||||
console.log('\n=== Request Analysis ===');
|
||||
console.log('1. Raw request body:', JSON.stringify(body, null, 2));
|
||||
|
||||
const { url } = body;
|
||||
if (!url) {
|
||||
return json({ error: 'URL is required' }, { status: 400 });
|
||||
// Handle YouTube URL request
|
||||
if (body.url) {
|
||||
console.log('2. Processing YouTube URL:', {
|
||||
url: body.url,
|
||||
language: body.language,
|
||||
hasLanguageParam: true
|
||||
});
|
||||
|
||||
// Extract video ID
|
||||
const match = body.url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
|
||||
const videoId = match ? match[1] : null;
|
||||
|
||||
if (!videoId) {
|
||||
return json({ error: 'Invalid YouTube URL' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('3. Video ID:', {
|
||||
id: videoId,
|
||||
language: body.language
|
||||
});
|
||||
|
||||
const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId);
|
||||
const transcript = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join(' ');
|
||||
|
||||
// Create response with transcript and language
|
||||
const response = {
|
||||
transcript,
|
||||
title: videoId,
|
||||
language: body.language
|
||||
};
|
||||
|
||||
console.log('4. Transcript processed:', {
|
||||
length: transcript.length,
|
||||
language: body.language,
|
||||
firstChars: transcript.substring(0, 50),
|
||||
responseSize: JSON.stringify(response).length
|
||||
});
|
||||
|
||||
return json(response);
|
||||
}
|
||||
|
||||
console.log('Fetching transcript for URL:', url);
|
||||
const transcriptData = await getTranscript(url);
|
||||
// Handle pattern execution request
|
||||
console.log('\n=== Server Request Analysis ===');
|
||||
console.log('1. Request overview:', {
|
||||
pattern: body.prompts?.[0]?.patternName,
|
||||
hasPrompts: !!body.prompts?.length,
|
||||
messageCount: body.messages?.length,
|
||||
isYouTube: body.url ? 'Yes' : 'No',
|
||||
language: body.language
|
||||
});
|
||||
|
||||
console.log('Successfully fetched transcript, preparing response');
|
||||
const response = json(transcriptData);
|
||||
// Ensure language instruction is present
|
||||
if (body.prompts?.[0] && body.language && body.language !== 'en') {
|
||||
const languageInstruction = `. Please use the language '${body.language}' for the output.`;
|
||||
if (!body.prompts[0].userInput?.includes(languageInstruction)) {
|
||||
body.prompts[0].userInput = (body.prompts[0].userInput || '') + languageInstruction;
|
||||
}
|
||||
}
|
||||
|
||||
// Log the actual response being sent
|
||||
const responseText = JSON.stringify(transcriptData);
|
||||
console.log('Sending response (first 200 chars):', responseText.slice(0, 200) + '...');
|
||||
console.log('2. Language analysis:', {
|
||||
input: body.prompts?.[0]?.userInput?.substring(0, 100),
|
||||
hasLanguageInstruction: body.prompts?.[0]?.userInput?.includes('language'),
|
||||
containsFr: body.prompts?.[0]?.userInput?.includes('fr'),
|
||||
containsEn: body.prompts?.[0]?.userInput?.includes('en'),
|
||||
requestLanguage: body.language
|
||||
});
|
||||
|
||||
// Log full request for debugging
|
||||
console.log('3. Full request:', JSON.stringify(body, null, 2));
|
||||
|
||||
// Log important fields
|
||||
console.log('4. Key fields:', {
|
||||
patternName: body.prompts?.[0]?.patternName,
|
||||
inputLength: body.prompts?.[0]?.userInput?.length,
|
||||
systemPromptLength: body.prompts?.[0]?.systemPrompt?.length,
|
||||
messageCount: body.messages?.length
|
||||
});
|
||||
|
||||
console.log('5. Sending to Fabric backend...');
|
||||
const fabricResponse = await fetch('http://localhost:8080/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
console.log('6. Fabric response:', {
|
||||
status: fabricResponse.status,
|
||||
ok: fabricResponse.ok,
|
||||
statusText: fabricResponse.statusText
|
||||
});
|
||||
|
||||
if (!fabricResponse.ok) {
|
||||
console.error('Error from Fabric API:', {
|
||||
status: fabricResponse.status,
|
||||
statusText: fabricResponse.statusText
|
||||
});
|
||||
throw new Error(`Fabric API error: ${fabricResponse.statusText}`);
|
||||
}
|
||||
|
||||
const stream = fabricResponse.body;
|
||||
if (!stream) {
|
||||
throw new Error('No response from fabric backend');
|
||||
}
|
||||
|
||||
// Create a TransformStream to inspect the data without modifying it
|
||||
const transformStream = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
const text = new TextDecoder().decode(chunk);
|
||||
if (text.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(text.slice(6));
|
||||
console.log('Stream chunk format:', {
|
||||
type: data.type,
|
||||
format: data.format,
|
||||
contentLength: data.content?.length
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Failed to parse stream chunk:', text);
|
||||
}
|
||||
}
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
});
|
||||
|
||||
// Pipe through the transform stream
|
||||
const transformedStream = stream.pipeThrough(transformStream);
|
||||
|
||||
// Return the transformed stream
|
||||
const response = new Response(transformedStream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Server error:', error);
|
||||
console.error('\n=== Error ===');
|
||||
console.error('Type:', error?.constructor?.name);
|
||||
console.error('Message:', error instanceof Error ? error.message : String(error));
|
||||
console.error('Stack:', error instanceof Error ? error.stack : 'No stack trace');
|
||||
return json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to fetch transcript' },
|
||||
{ error: error instanceof Error ? error.message : 'Failed to process request' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
// Get the absolute path to the inbox directory
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const inboxPath = join(__dirname, '..', '..', 'lib', 'content', 'inbox', filename);
|
||||
// const inboxPath = join(__dirname, '..', 'myfiles', 'inbox', filename);
|
||||
// New version using environment variables:
|
||||
// const inboxPath = join(process.env.DATA_DIR || './web/myfiles', 'inbox', filename);
|
||||
const inboxPath = join(__dirname, '..', '..', '..', 'myfiles', 'inbox', filename);
|
||||
|
||||
await writeFile(inboxPath, content, 'utf-8');
|
||||
|
||||
|
||||
105
web/src/routes/obsidian/+server.ts
Normal file
105
web/src/routes/obsidian/+server.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
interface ObsidianRequest {
|
||||
pattern: string;
|
||||
noteName: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function escapeShellArg(arg: string): string {
|
||||
// Replace single quotes with '\'' and wrap in single quotes
|
||||
return `'${arg.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
let tempFile: string | undefined;
|
||||
|
||||
try {
|
||||
// Parse and validate request
|
||||
const body = await request.json() as ObsidianRequest;
|
||||
if (!body.pattern || !body.noteName || !body.content) {
|
||||
return json(
|
||||
{ error: 'Missing required fields: pattern, noteName, or content' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\n=== Obsidian Request ===');
|
||||
console.log('1. Pattern:', body.pattern);
|
||||
console.log('2. Note name:', body.noteName);
|
||||
console.log('3. Content length:', body.content.length);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Format content with markdown code blocks
|
||||
const formattedContent = `\`\`\`markdown\n${body.content}\n\`\`\``;
|
||||
const escapedFormattedContent = escapeShellArg(formattedContent);
|
||||
|
||||
// Generate file name and path
|
||||
const fileName = `${new Date().toISOString().split('T')[0]}-${body.noteName}.md`;
|
||||
|
||||
const obsidianDir = 'myfiles/Fabric_obsidian';
|
||||
const filePath = `${obsidianDir}/${fileName}`;
|
||||
await execAsync(`mkdir -p "${obsidianDir}"`);
|
||||
console.log('4. Ensured Obsidian directory exists');
|
||||
|
||||
|
||||
// Create temp file
|
||||
tempFile = `/tmp/fabric-${Date.now()}.txt`;
|
||||
|
||||
// Write formatted content to temp file
|
||||
await execAsync(`echo ${escapedFormattedContent} > "${tempFile}"`);
|
||||
console.log('5. Wrote formatted content to temp file');
|
||||
|
||||
// Copy from temp file to final location (safer than direct write)
|
||||
await execAsync(`cp "${tempFile}" "${filePath}"`);
|
||||
console.log('6. Copied content to final location:', filePath);
|
||||
|
||||
// Verify file was created and has content
|
||||
const { stdout: lsOutput } = await execAsync(`ls -l "${filePath}"`);
|
||||
const { stdout: wcOutput } = await execAsync(`wc -l "${filePath}"`);
|
||||
console.log('7. File verification:', lsOutput);
|
||||
console.log('8. Line count:', wcOutput);
|
||||
|
||||
// Return success response with file details
|
||||
return json({
|
||||
success: true,
|
||||
fileName,
|
||||
filePath,
|
||||
message: `Successfully saved to ${fileName}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n=== Error ===');
|
||||
console.error('Type:', error?.constructor?.name);
|
||||
console.error('Message:', error instanceof Error ? error.message : String(error));
|
||||
console.error('Stack:', error instanceof Error ? error.stack : 'No stack trace');
|
||||
|
||||
return json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Failed to process request',
|
||||
details: error instanceof Error ? error.stack : undefined
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
|
||||
} finally {
|
||||
// Clean up temp file if it exists
|
||||
if (tempFile) {
|
||||
try {
|
||||
await execAsync(`rm -f "${tempFile}"`);
|
||||
console.log('9. Cleaned up temp file');
|
||||
} catch (cleanupError) {
|
||||
console.error('Failed to clean up temp file:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB |
1721
web/static/data/pattern_descriptions.json
Normal file
1721
web/static/data/pattern_descriptions.json
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 491 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@@ -38,9 +38,6 @@ const mdsvexOptions = {
|
||||
backticks: true,
|
||||
dashes: true,
|
||||
},
|
||||
layout: {
|
||||
_: './src/lib/components/posts/PostLayout.svelte',
|
||||
},
|
||||
highlight: {
|
||||
highlighter: async (code, lang) => {
|
||||
try {
|
||||
@@ -72,15 +69,25 @@ const mdsvexOptions = {
|
||||
const config = {
|
||||
extensions: ['.svelte', '.md', '.svx'],
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
// You can add adapter-specific options here
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: null,
|
||||
precompress: false,
|
||||
strict: true
|
||||
}),
|
||||
prerender: {
|
||||
handleHttpError: ({ path, referrer, message }) => {
|
||||
// ignore 404
|
||||
// Log the error for debugging
|
||||
console.warn(`HTTP error during prerendering: ${message}\nPath: ${path}\nReferrer: ${referrer}`);
|
||||
|
||||
// ignore 404 for specific case
|
||||
if (path === '/not-found' && referrer === '/') {
|
||||
return warn;
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise fiail
|
||||
// otherwise fail
|
||||
throw new Error(message);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,7 +28,31 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
timeout: 30000,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('error', (err, req, res) => {
|
||||
console.log('proxy error', err);
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'text/plain',
|
||||
});
|
||||
res.end('Something went wrong. The backend server may not be running.');
|
||||
});
|
||||
}
|
||||
},
|
||||
'^/(patterns|models|sessions)/names': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
timeout: 30000,
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('error', (err, req, res) => {
|
||||
console.log('proxy error', err);
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Backend server not running', names: [] }));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -36,5 +60,5 @@ export default defineConfig({
|
||||
interval: 100,
|
||||
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/.svelte-kit/**']
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user