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:
Eugen Eisler
2025-02-24 21:09:23 +01:00
committed by GitHub
70 changed files with 7662 additions and 8021 deletions

5
.gitignore vendored
View File

@@ -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
View File

@@ -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

View 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

View 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()

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -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.

View 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;
}

View File

@@ -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

View 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

View File

@@ -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"
}

View File

View File

7411
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View 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);
}
};
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) {

View 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>

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View 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>

View 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>

View 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>

View File

@@ -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(

View 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>

View File

@@ -0,0 +1,4 @@
import Checkbox from './Checkbox.svelte';
export { Checkbox };
export default Checkbox;

View 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>

View File

@@ -1,6 +1,4 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};
import Input from './Input.svelte';
export { Input };
export default Input;

View File

@@ -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
)}
/>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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(() => {

View 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>

View 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
}));
}

View 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>

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -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

View 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();

View 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 };

View File

@@ -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';

View File

@@ -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
})
};
}

View 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`;
}

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -0,0 +1,4 @@
export interface Pattern {
patternName: string;
description: string;
}

View File

@@ -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}

View 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 }
);
}
};

View File

@@ -39,4 +39,4 @@
overflow: hidden;
}
</style>

View File

@@ -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 }
);
}

View File

@@ -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');

View 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

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

View File

@@ -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);
},
},

View File

@@ -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/**']
}
}
},
});