Compare commits

...

59 Commits

Author SHA1 Message Date
github-actions[bot]
ce9d4ad831 Update version to v1.4.159 and commit 2025-03-16 19:23:15 +00:00
Daniel Miessler
657bcab48c Added flashcard generator. 2025-03-16 12:21:50 -07:00
github-actions[bot]
cd11dcc7a9 Update version to v1.4.158 and commit 2025-03-16 17:26:52 +00:00
Eugen Eisler
22040a42f2 Merge pull request #1367 from ksylvan/fix/code_cleanup
Remove Generic Type Parameters from StorageHandler Initialization
2025-03-16 18:25:33 +01:00
Kayvan Sylvan
705ccd750b refactor: remove generic type parameters from NewStorageHandler calls
## CHANGES

- Remove explicit type parameters from StorageHandler initialization
- Update contexts handler constructor implementation
- Update patterns handler constructor implementation
- Update sessions handler constructor implementation
- Simplify API by relying on type inference
2025-03-16 09:21:17 -07:00
github-actions[bot]
db7c2b70cb Update version to v1.4.157 and commit 2025-03-16 07:36:39 +00:00
Eugen Eisler
9dc9bfa1d5 Merge pull request #1365 from ksylvan/feature/strategies
Implement Prompt Strategies in Fabric
2025-03-16 08:35:23 +01:00
Kayvan Sylvan
6b93658191 chore: remove redundant yt function definition 2025-03-15 17:35:45 -07:00
Kayvan Sylvan
ea7a425a26 add newline to end of cod.json 2025-03-15 13:18:53 -07:00
Kayvan Sylvan
9582978adb Fix help message when no strategies found. 2025-03-15 13:05:51 -07:00
Kayvan Sylvan
453d8e75e4 fix: fix handling of the installed strategies dir 2025-03-15 09:36:34 -07:00
Kayvan Sylvan
901a010efd chore: remove fallback to local strategies directory if missing 2025-03-15 08:29:58 -07:00
Kayvan Sylvan
f744e25b39 change [optional] to [required] in strategies 2025-03-15 00:33:58 -07:00
Kayvan Sylvan
096f40df68 feat: add prompt strategies and improve installation documentation
## CHANGES

- Add prompt strategies like Chain of Thought (CoT)
- Implement strategy selection with `--strategy` flag
- Improve README with platform-specific installation instructions
- Fix web interface documentation link
- Refactor git operations with new githelper package
- Add `--liststrategies` command to view available strategies
- Support applying strategies to system prompts
- Fix YouTube configuration check
- Improve error handling in session management
2025-03-15 00:30:30 -07:00
github-actions[bot]
29f4534001 Update version to v1.4.156 and commit 2025-03-11 09:05:45 +00:00
Eugen Eisler
ed9324d611 Merge pull request #1356 from ksylvan/main
chore: add .vscode to `.gitignore` and fix typos and markdown linting  in `Alma.md`
2025-03-11 10:04:32 +01:00
Eugen Eisler
438b3c5211 Merge pull request #1352 from matmilbury/patch-1
pattern_explanations.md: fix typo
2025-03-11 07:52:48 +01:00
Eugen Eisler
efeeb7a796 Merge pull request #1354 from jmd1010/chat-history-window-sizing
Fix Chat history window scrolling behavior
2025-03-11 07:51:51 +01:00
Kayvan Sylvan
6b1ff0ab21 chore: add .vscode to .gitignore and fix typos and markdown linting in Alma.md 2025-03-10 09:29:16 -07:00
jmd1010
acb925f5a9 Update Web V2 Install Guide with improved instructions 2025-03-09 15:27:02 -04:00
jmd1010
761293ede7 Fix Chat history window sizing 2025-03-09 14:58:45 -04:00
Mat Milbury
e004e50037 pattern_explanations.md: fix typo 2025-03-09 16:11:14 +01:00
github-actions[bot]
44a6c03bc8 Update version to v1.4.155 and commit 2025-03-09 09:01:41 +00:00
Eugen Eisler
d794afe405 Merge pull request #1350 from jmd1010/pattern-search-implementation
Implement Pattern Tile search functionality
2025-03-09 10:00:28 +01:00
github-actions[bot]
e4ac322227 Update version to v1.4.154 and commit 2025-03-09 08:56:50 +00:00
Eugen Eisler
1fc19da19f Merge pull request #1349 from ksylvan/03-08-extra-version-declaration-removed
Fix: v1.4.153 does not compile because of extra version declaration
2025-03-09 09:55:37 +01:00
jmd1010
b213068680 Implement column resize functionnality 2025-03-08 17:34:49 -05:00
jmd1010
bf3af207b9 Implement Pattern Tile search functionality 2025-03-08 12:56:55 -05:00
Kayvan Sylvan
e28ba224b5 fix: update Azure client API version access path in tests 2025-03-08 09:52:20 -08:00
Kayvan Sylvan
5b7697c5ab chore: remove unnecessary version variable from main.go 2025-03-08 09:29:20 -08:00
github-actions[bot]
0701b7d263 Update version to v1.4.153 and commit 2025-03-08 08:56:19 +00:00
Eugen Eisler
aac29025fb Merge pull request #1348 from liyuankui/feature/add-litellm-vendor
feat: Add LiteLLM AI plugin support with local endpoint configuration
2025-03-08 09:55:08 +01:00
kyle
6928f9a312 feat: Add LiteLLM AI plugin support with local endpoint configuration 2025-03-08 11:37:54 +08:00
github-actions[bot]
ef2e985d3f Update version to v1.4.152 and commit 2025-03-07 08:23:45 +00:00
Eugen Eisler
1df945556d fix: Fix pipe handling 2025-03-07 09:22:26 +01:00
github-actions[bot]
b6f313db8f Update version to v1.4.151 and commit 2025-03-07 07:47:35 +00:00
Eugen Eisler
cb00f2026e Merge pull request #1339 from Eckii24/feature/add-azure-api-version
Feature/add azure api version
2025-03-07 08:46:20 +01:00
github-actions[bot]
bd39d98ffc Update version to v1.4.150 and commit 2025-03-07 07:44:52 +00:00
Eugen Eisler
36b0afa692 Merge pull request #1343 from jmd1010/input-component-update
Rename input.svelte to Input.svelte for proper component naming convention
2025-03-07 08:43:32 +01:00
jmd1010
53d09d8a5a Rename input.svelte to Input.svelte for proper component naming convention 2025-03-06 21:08:30 -05:00
github-actions[bot]
703144edad Update version to v1.4.149 and commit 2025-03-05 12:50:38 +00:00
Eugen Eisler
717e50e570 Merge pull request #1340 from ksylvan/03-04-youtube-live-fix
Fix for youtube live links plus new youtube_summary pattern
2025-03-05 13:49:22 +01:00
Eugen Eisler
4af6d5eeed Merge pull request #1338 from jmd1010/directory-structure-update
Update Web V2 Install Guide layout
2025-03-05 10:09:23 +01:00
Kayvan Sylvan
de356ddeb5 chore: update version 2025-03-04 22:00:26 -08:00
Kayvan Sylvan
59c50def2a feat: update yt commands and docs to support timestamped transcripts
CHANGES
- Add argument validation to yt for usage errors
- Enable -t flag for transcript with timestamps
- Refactor PowerShell yt function with parameter switch
- Update README to dynamically select transcript option
- Document youtube_summary feature in pattern explanations
- Introduce youtube_summary pattern.
2025-03-04 21:46:50 -08:00
Kayvan Sylvan
1dafb09e07 remove spurious newline 2025-03-04 21:20:24 -08:00
Kayvan Sylvan
e8caf9fc10 feat: update YouTube regex to support live URLs 2025-03-04 21:18:51 -08:00
jmd1010
59537c4bf5 Update Web V2 Install Guide layout 2 2025-03-04 12:29:31 -05:00
Eckii24
231516917d Update azure.go 2025-03-04 17:39:56 +01:00
Eckii24
58d17fd0ec Update openai.go 2025-03-04 17:37:19 +01:00
Eckii24
8bd4aa6d1a Update azure_test.go 2025-03-04 17:36:37 +01:00
Eckii24
629c1b3e11 Update azure.go 2025-03-04 17:36:11 +01:00
jmd1010
2f4569177d Update Web V2 Install Guide layout 2025-03-04 10:58:48 -05:00
Eugen Eisler
f2b85af0ea Merge pull request #1330 from jmd1010/directory-structure-update
Fixed ALL CAP DIR as requested and processed minor updates to documentation
2025-03-04 12:17:03 +01:00
Eugen Eisler
36be4c747c Merge pull request #1333 from asasidh/main
Update QUOTES section to include speaker names for clarity
2025-03-04 09:35:53 +01:00
asasidh
be6049e577 Update QUOTES section to include speaker names for clarity 2025-03-02 17:42:52 -06:00
jmd1010
a8ae09d4d6 Update Web V2 Install Guide with improved instructions V2 2025-02-28 19:57:13 -05:00
jmd1010
30059c46a0 Update Web V2 Install Guide with improved instructions 2025-02-28 13:23:43 -05:00
jmd1010
2d10c71e39 Reorganize documentation with consistent directory naming and updated guides 2025-02-28 11:34:17 -05:00
48 changed files with 2412 additions and 1170 deletions

3
.gitignore vendored
View File

@@ -347,3 +347,6 @@ ENV
web/package-lock.json
.gitignore_backup
web/static/*.png
# Local VSCode project settings
.vscode/

52
Alma.md
View File

@@ -1,3 +1,5 @@
# SPQA Policy and State for Alma Security
## Document Purpose
This document captures the SPQA policy and State for Alma Security, a security startup out of Redwood City, Ca.
@@ -22,7 +24,7 @@ The mission of Alma Security is to ensure businesses can continuously authentica
## Company Goals (G1 means goal 1, G2 is goal 2, etc. Treat each item (goal/kpi/etc) as half as important as the one before it.)
NOTE: Some goals are things like project rollouts which serve the higher goals. In that case they shouldn't always be considered so much lower priority because one is serving the other.
NOTE: Some goals are things like project rollout which serve the higher goals. In that case they shouldn't always be considered so much lower priority because one is serving the other.
## Company Goals
@@ -37,7 +39,7 @@ NOTE: Some goals are things like project rollouts which serve the higher goals.
## Company KPIs
- K1: Current marketshare percentage
- K1: Current market share percentage
- K2: Number of active customers
- K3: Current churn percentage
- K4: Launched_in_Europe (yes/no)
@@ -45,22 +47,22 @@ NOTE: Some goals are things like project rollouts which serve the higher goals.
-----------------------------------------------------------------------------------------------------------------------
## Security Team Mission
## Security Team Mission
- SM1: Protect Alma Security's customers and intellectual property from security and privacy incidents.
## Security Team Goals
- SG1: Secure all customer data -- especially biometric -- from security and privacy incidents.
- SG1: Secure all customer data -- especially biometric -- from security and privacy incidents.
- SG2: Protect Alma Security's intellectual property from being captured by unauthorized parties.
- SG3: Reach a time to detect malicious behavior of less than 4 minutes by January 2025
- SG4: Ensure the public trusts our product, because it's an authentication product we can't survive if people don't trust us.
- SG5: Reach a time to remediate critical vulnerabilties on crown jewel systems of less than 16 hours by August 2025
- SG6: Reach a time to remediate critical vulnerabilties on all systems of less than 3 days by August 2025
- SG5: Reach a time to remediate critical vulnerabilities on crown jewel systems of less than 16 hours by August 2025
- SG6: Reach a time to remediate critical vulnerabilities on all systems of less than 3 days by August 2025
- SG5: Reach a time to remediate critical vulnerabilities on crown jewel systems of less than 16 hours by August 2025
- SG6: Reach a time to remediate critical vulnerabilities on all systems of less than 3 days by August 2025
- SG7: Complete audit of Apple Passkey integration by February 2025
- SG8: Complete remediation of Apple Passkey vulns by February 2025
- SG8: Complete remediation of Apple Passkey vulnerabilities by February 2025
## Security Team KPIs (How we measure the team)
@@ -72,7 +74,7 @@ NOTE: Some goals are things like project rollouts which serve the higher goals.
## Risk Register (The things we're most worried about)
- R1: Our infrastructure security team is understaffed by 50% after 5 key people left
- R1: Our infrastructure security team is understaffed by 50% after 5 key people left
- R2: We are not currently monitoring our external perimeter for attack surface related vulnerabilities like open ports, listening applications, unknown hosts, unknown subdomains pointing to these things, etc. We only do scans once every couple of months and we don't really have anyone to look at the results
- R3: It takes us multiple days to investigate potential malicious behavior on our systems.
- R4: We lack a full list of our assets, including externally facing hosts, S3 buckets, etc., which make up our attack surface
@@ -92,7 +94,7 @@ So our risk register looks like this:
2. Our perimeter is not being monitored for attack surface related vulnerabilities
3. It takes us too long to detect and start investigating malicious behavior on our systems
4. We do not have a full list of our assets, which makes it difficult to know what we need to protect
5. We have a low public trust score due to the events of 2022
5. We have a low public trust score due to the events of 2022
### Strategies
@@ -136,14 +138,14 @@ $INSERT GRAPHS OF KPI PROGRESS OVER TIME HERE$
- alma.amazon-domain.com is our primary S3 bucket that contains everything, but it's not public readable or listable
- We have a root account for our AWS account that doesn't yet have 2FA on it, but we're working on fixing that within a few weeks (but it's been open for a few months)
- We also use Postgres for all our databases.
- Developers have root access to the all kubernetes nodes via SSH on port 45,001 using a shared developer key issued during laptop provisioning.
- Developers have root access to the all kubernetes nodes via SSH on port 45,001 using a shared developer key issued during laptop provisioning.
- We're a kubernetes shop and do everything through AWS
- We're logging most stuff to Cloudtrail and we kind of use guarduty, but we don't have a 24/7 team to monitor alerts and logs. We should add that to our list of challenges next time we update our overarll policy
- We're logging most stuff to Cloudtrail and we kind of use Amazon Guarduty, but we don't have a 24/7 team to monitor alerts and logs. We should add that to our list of challenges next time we update our overall policy
- We also have a Windows infrastructure because some key personnel came from Microsoft. The DC is hosted in our head office which is in Redwood City, and anyone who works in that office (most of the 300 employees) uses that to log in when they start work. The domain is ALMA.
- There's a domain-joined fileserver running Windows 2012 that most people use to upload new ideas and plans for new products. It uses Windows authentication from the domain.
- We use a palo alto firewall with 2fa using windows authenticator tied to SSO.
- There's a domain-joined fileserver running Windows 2012 that most people use to upload new ideas and plans for new products. It uses Windows authentication from the domain.
- We use a Palo Alto Networks firewall with 2fa using windows authenticator tied to SSO.
- The name of the AI system doing all this context creation using SPQA is Alma, which is also the name of the company.
- We use Workday for HR stuff. Slack for realtime communications. Outlook 365 as a service. Sentinel One on the workstations and laptops. Servers in AWS are mostly Amazon Linux 2 with a few Ubuntu boxes that are a few years old.
- We use Workday for HR stuff. Slack for realtime communications. Outlook 365 as a service. Sentinel One on the workstations and laptops. Servers in AWS are mostly Amazon Linux 2 with a few Ubuntu boxes that are a few years old.
- We also primarily use Postgres for all of our systems.
## Team
@@ -160,19 +162,19 @@ PROJECT NAME | PROJECT DESCRIPTION | PROJECT PRIORITY | PROJECT MEMBERS | START
WAF Install | Install a WAF in front of our main web app | Critical | Nadia Khan | 2024-01-01 - Ongoing | In Progress | $112K one-time, $9K/month
Multi-Factor Authentication (MFA) Rollout | Implement MFA across all internal and external systems | Critical | Chris Magaan | 2024-01-15 | 2024-05-01 | Planned | $80K one-time, $5K/month
Multi-Factor Authentication (MFA) Rollout | Implement MFA across all internal and external systems | Critical | Chris Magann | 2024-01-15 | 2024-05-01 | Planned | $80K one-time, $5K/month
Procure and Implement ASM | Implement continuous monitoring for attack surface vulnerabilities | High | Tigan Wang | 2024-02-15 | 2024-06-15 | Not Started | $75K one-time, $6K/month
Procure and Implement ASM | Implement continuous monitoring for attack surface vulnerabilities | High | Tigan Wang | 2024-02-15 | 2024-06-15 | Not Started | $75K one-time, $6K/month
Data Encryption Upgrade | Upgrade encryption protocols for all sensitive data | Medium | Nadia Khan | 2024-04-01 | 2024-08-01 | Planned | $95K one-time
Data Encryption Upgrade | Upgrade encryption protocols for all sensitive data | Medium | Nadia Khan | 2024-04-01 | 2024-08-01 | Planned | $95K one-time
Incident Response Enhancement | Develop and implement a 24/7 incident response team | High | Nadia Khan | 2024-03-01 | 2024-07-01 | In Progress | $150K one-time, $10K/month
Incident Response Enhancement | Develop and implement a 24/7 incident response team | High | Nadia Khan | 2024-03-01 | 2024-07-01 | In Progress | $150K one-time, $10K/month
Cloud Security Optimization | Optimize AWS cloud security configurations and practices | Medium | Tigan Wang | 2024-02-01 | 2024-06-01 | In Progress | $100K one-time, $8K/month
Cloud Security Optimization | Optimize AWS cloud security configurations and practices | Medium | Tigan Wang | 2024-02-01 | 2024-06-01 | In Progress | $100K one-time, $8K/month
S3 Bucket Security | Review and secure all S3 buckets to prevent data breaches | High | Chris Magaan | 2024-01-10 | 2024-04-10 | In Progress | $70K one-time, $5K/month
S3 Bucket Security | Review and secure all S3 buckets to prevent data breaches | High | Chris Magann | 2024-01-10 | 2024-04-10 | In Progress | $70K one-time, $5K/month
SQL Injection Mitigation | Implement measures to eliminate SQL injection vulnerabilities | High | Tigan Wang | 2024-01-20 | 2024-05-20 | Not Started | $60K one-time
SQL Injection Mitigation | Implement measures to eliminate SQL injection vulnerabilities | High | Tigan Wang | 2024-01-20 | 2024-05-20 | Not Started | $60K one-time
## SECURITY POSTURE (To be referenced for compliance questions and security questionnaires)
@@ -286,7 +288,9 @@ First draft of the incident response plan created, but not tested.
June 2019
Enforced MFA for Google Workspace admin accounts; standard user
## CURRENT STATE (KPIs, Metrics, Project Activity Updates, etc.)
- October 2022: Current time to detect malicious behavior is 81 hours
- October 2022: Current time to start investigating malicious behavior is 82 hours
- October 2022: Current time to remediate critical vulnerabilities on crown jewel systems is 21 days
@@ -308,7 +312,7 @@ Enforced MFA for Google Workspace admin accounts; standard user
- January 2024: Current time to start investigating malicious behavior is 14 hours
- January 2024: Current time to remediate critical vulnerabilities on crown jewel systems is 8 days
- January 2024: Current time to remediate critical vulnerabilities on all systems is 12 days
- March 2024: We're now remediating crits on crown jewels in less than 6 days
- April 2024: We're now remediating all criticals within 11 days
- July 2024: Criticals are now being fixed in 9 days
- March 2024: We're now remediating critical vulnerabilities on crown jewels in less than 6 days
- April 2024: We're now remediating all critical vulnerabilities within 11 days
- July 2024: critical vulnerabilities are now being fixed in 9 days
- On August 5 we got remediation of critical vulnerabilities down to 7 days

File diff suppressed because it is too large Load Diff

167
README.md
View File

@@ -34,13 +34,18 @@
- [`fabric`](#fabric)
- [Navigation](#navigation)
- [Updates](#updates)
- [Intro videos](#intro-videos)
- [What and why](#what-and-why)
- [Intro videos](#intro-videos)
- [Philosophy](#philosophy)
- [Breaking problems into components](#breaking-problems-into-components)
- [Too many prompts](#too-many-prompts)
- [Installation](#installation)
- [Get Latest Release Binaries](#get-latest-release-binaries)
- [Windows:](#windows)
- [MacOS (arm64):](#macos-arm64)
- [MacOS (amd64):](#macos-amd64)
- [Linux (amd64):](#linux-amd64)
- [Linux (arm64):](#linux-arm64)
- [From Source](#from-source)
- [Environment Variables](#environment-variables)
- [Setup](#setup)
@@ -52,12 +57,15 @@
- [Our approach to prompting](#our-approach-to-prompting)
- [Examples](#examples)
- [Just use the Patterns](#just-use-the-patterns)
- [Prompt Strategies](#prompt-strategies)
- [Custom Patterns](#custom-patterns)
- [Helper Apps](#helper-apps)
- [`to_pdf`](#to_pdf)
- [`to_pdf` Installation](#to_pdf-installation)
- [pbpaste](#pbpaste)
- [Web Interface](#Web_Interface)
- [Web Interface](#web-interface)
- [Installing](#installing)
- [Streamlit UI](#streamlit-ui)
- [Meta](#meta)
- [Primary contributors](#primary-contributors)
@@ -204,8 +212,19 @@ for pattern_file in $HOME/.config/fabric/patterns/*; do
done
yt() {
if [ "$#" -eq 0 ] || [ "$#" -gt 2 ]; then
echo "Usage: yt [-t | --timestamps] youtube-link"
echo "Use the '-t' flag to get the transcript with timestamps."
return 1
fi
transcript_flag="--transcript"
if [ "$1" = "-t" ] || [ "$1" = "--timestamps" ]; then
transcript_flag="--transcript-with-timestamps"
shift
fi
local video_link="$1"
fabric -y "$video_link" --transcript
fabric -y "$video_link" $transcript_flag
}
```
@@ -263,10 +282,34 @@ function $patternName {
function yt {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[Parameter()]
[Alias("timestamps")]
[switch]$t,
[Parameter(Position = 0, ValueFromPipeline = $true)]
[string]$videoLink
)
fabric -y $videoLink --transcript
begin {
$transcriptFlag = "--transcript"
if ($t) {
$transcriptFlag = "--transcript-with-timestamps"
}
}
process {
if (-not $videoLink) {
Write-Error "Usage: yt [-t | --timestamps] youtube-link"
return
}
}
end {
if ($videoLink) {
# Execute and allow output to flow through the pipeline
fabric -y $videoLink $transcriptFlag
}
}
}
```
@@ -306,11 +349,6 @@ for pattern_file in ~/.config/fabric/patterns/*; do
}
"
done
yt() {
local video_link="$1"
fabric -y "$video_link" --transcript
}
```
This will allow you to use the patterns as aliases like in the above for example `summarize` instead of `fabric --pattern summarize --stream`, however if you pass in an extra argument like this `summarize "my_article_title"` your output will be saved in the destination that you set in `obsidian_base="/path/to/obsidian"` in the following format `YYYY-MM-DD-my_article_title.md` where the date gets autogenerated for you.
@@ -356,48 +394,60 @@ Usage:
fabric [OPTIONS]
Application Options:
-p, --pattern= Choose a pattern from the available patterns
-v, --variable= Values for pattern variables, e.g. -v=#role:expert -v=#points:30"
-C, --context= Choose a context from the available contexts
--session= Choose a session from the available sessions
-a, --attachment= Attachment path or URL (e.g. for OpenAI image recognition messages)
-S, --setup Run setup for all reconfigurable parts of fabric
-t, --temperature= Set temperature (default: 0.7)
-T, --topp= Set top P (default: 0.9)
-s, --stream Stream
-P, --presencepenalty= Set presence penalty (default: 0.0)
-r, --raw Use the defaults of the model without sending chat options (like temperature etc.) and use the user role instead of the system role for patterns.
-F, --frequencypenalty= Set frequency penalty (default: 0.0)
-l, --listpatterns List all patterns
-L, --listmodels List all available models
-x, --listcontexts List all contexts
-X, --listsessions List all sessions
-U, --updatepatterns Update patterns
-c, --copy Copy to clipboard
-m, --model= Choose model
-o, --output= Output to file
--output-session Output the entire session (also a temporary one) to the output file
-n, --latest= Number of latest patterns to list (default: 0)
-d, --changeDefaultModel Change default model
-y, --youtube= YouTube video "URL" to grab transcript, comments from it and send to chat
--transcript Grab transcript from YouTube video and send to chat (it used per default).
--comments Grab comments from YouTube video and send to chat
--metadata Grab metadata from YouTube video and send to chat
-g, --language= Specify the Language Code for the chat, e.g. -g=en -g=zh
-u, --scrape_url= Scrape website URL to markdown using Jina AI
-q, --scrape_question= Search question using Jina AI
-e, --seed= Seed to be used for LMM generation
-w, --wipecontext= Wipe context
-W, --wipesession= Wipe session
--printcontext= Print context
--printsession= Print session
--readability Convert HTML input into a clean, readable view
--serve Initiate the API server
--dry-run Show what would be sent to the model without actually sending it
--version Print current version
-p, --pattern= Choose a pattern from the available patterns
-v, --variable= Values for pattern variables, e.g. -v=#role:expert -v=#points:30
-C, --context= Choose a context from the available contexts
--session= Choose a session from the available sessions
-a, --attachment= Attachment path or URL (e.g. for OpenAI image recognition messages)
-S, --setup Run setup for all reconfigurable parts of fabric
-t, --temperature= Set temperature (default: 0.7)
-T, --topp= Set top P (default: 0.9)
-s, --stream Stream
-P, --presencepenalty= Set presence penalty (default: 0.0)
-r, --raw Use the defaults of the model without sending chat options (like temperature etc.) and use the user role instead of the system role for patterns.
-F, --frequencypenalty= Set frequency penalty (default: 0.0)
-l, --listpatterns List all patterns
-L, --listmodels List all available models
-x, --listcontexts List all contexts
-X, --listsessions List all sessions
-U, --updatepatterns Update patterns
-c, --copy Copy to clipboard
-m, --model= Choose model
--modelContextLength= Model context length (only affects ollama)
-o, --output= Output to file
--output-session Output the entire session (also a temporary one) to the output file
-n, --latest= Number of latest patterns to list (default: 0)
-d, --changeDefaultModel Change default model
-y, --youtube= YouTube video or play list "URL" to grab transcript, comments from it and send to chat or print it put to the console and store it in the output file
--playlist Prefer playlist over video if both ids are present in the URL
--transcript Grab transcript from YouTube video and send to chat (it is used per default).
--transcript-with-timestamps Grab transcript from YouTube video with timestamps and send to chat
--comments Grab comments from YouTube video and send to chat
--metadata Output video metadata
-g, --language= Specify the Language Code for the chat, e.g. -g=en -g=zh
-u, --scrape_url= Scrape website URL to markdown using Jina AI
-q, --scrape_question= Search question using Jina AI
-e, --seed= Seed to be used for LMM generation
-w, --wipecontext= Wipe context
-W, --wipesession= Wipe session
--printcontext= Print context
--printsession= Print session
--readability Convert HTML input into a clean, readable view
--input-has-vars Apply variables to user input
--dry-run Show what would be sent to the model without actually sending it
--serve Serve the Fabric Rest API
--serveOllama Serve the Fabric Rest API with ollama endpoints
--address= The address to bind the REST API (default: :8080)
--config= Path to YAML config file
--version Print current version
--listextensions List all registered extensions
--addextension= Register a new extension from config file path
--rmextension= Remove a registered extension by name
--strategy= Choose a strategy from the available strategies
--liststrategies List all strategies
Help Options:
-h, --help Show this help message
-h, --help Show this help message
```
@@ -468,6 +518,21 @@ You can use any of the Patterns you see there in any AI application that you hav
The wisdom of crowds for the win.
### Prompt Strategies
Fabric also implements prompt strategies like "Chain of Thought" or "Chain of Draft" which can
be used in addition to the basic patterns.
See the [Thinking Faster by Writing Less](https://arxiv.org/pdf/2502.18600) paper and
the [Thought Generation section of Learn Prompting](https://learnprompting.org/docs/advanced/thought_generation/introduction) for examples of prompt strategies.
Each strategy is available as a small `json` file in the [`/strategies`](https://github.com/danielmiessler/fabric/tree/main/strategies) directory.
The prompt modification of the strategy is applied to the system prompt and passed on to the
LLM in the chat session.
Use `fabric -S` and select the option to install the strategies in your `~/.config/fabric` directory.
## Custom Patterns
You may want to use Fabric to create your own custom Patterns—but not share them with others. No problem!
@@ -540,7 +605,7 @@ alias pbpaste='xclip -selection clipboard -o'
## Web Interface
Fabric now includes a built-in web interface that provides a GUI alternative to the command-line interface and an out-of-the-box website for those who want to get started with web development or blogging.
Fabric now includes a built-in web interface that provides a GUI alternative to the command-line interface and an out-of-the-box website for those who want to get started with web development or blogging.
You can use this app as a GUI interface for Fabric, a ready to go blog-site, or a website template for your own projects.
The `web/src/lib/content` directory includes starter `.obsidian/` and `templates/` directories, allowing you to open up the `web/src/lib/content/` directory as an [Obsidian.md](https://obsidian.md) vault. You can place your posts in the posts directory when you're ready to publish.

View File

@@ -1,41 +1,43 @@
# How to Install Fabric, Web Interface and PDF-to-Markdown
# How to Install the Web Interface and PDF-to-Markdown
Until it is fully integrated in GO, here's a safe way to test Web V2.
If Fabric is already installed and you see fabric/web, go to step 3
## Step 1: Clone the Repository
If fabric is not installed, ensure Go is installed https://go.dev/doc/install and node / npm for web https://nodejs.org/en/download.
Clone the fabric repository to your desired location:
There are many ways to install fabric. Here's one approach that usually works well:
```bash
# For Mac (specific location)
git clone https://github.com/danielmiessler/fabric.git ~/Documents/Github/fabric
# Or from any parent directory
## Step 1: clone the repo
In terminal, from the parent directory where you want to install fabric:
git clone https://github.com/danielmiessler/fabric.git
## Step 2 : Install Fabric
cd fabric
go install github.com/danielmiessler/fabric@latest
Step 2: Install GUI
## Step 3: Install GUI
Navigate to the web directory and install dependencies:
cd web
npm install
npx svelte-kit sync
Step 3: Install PDF-to-Markdown
## Step 4: Install PDF-to-Markdown
Install the PDF conversion components in the correct order:
# Make sure you're in the fabric/web directory
cd fabric/web
cd web
# Install dependencies in this specific order
npm install -D patch-package
npm install -D pdfjs-dist@2.5.207
npm install -D github:jzillmann/pdf-to-markdown#modularize
No build step is required after installation.
Step 4: Update Shell Configuration
## Step 5: Update Shell Configuration if not already done from your fabric installation
For Mac/Linux users:
Add environment variables to your ~/.bashrc (Linux) or ~/.zshrc (Mac) file:
@@ -55,8 +57,19 @@ REFER TO OFFICIAL FABRIC README.MD FILE FOR OTHER OPERATING SYSTEMS
Step 5: Create Aliases for Patterns
Add the following to your .zshrc or .bashrc file to create shorter commands:
# Define the base directory for Obsidian notes,
obsidian_base="/path/to/obsidian"
```bash
# The following three lines of code are path examples, replace with your actual path.
# Add fabric to PATH
export PATH="/Users/USERNAME/Documents/fabric:$PATH"
# Define the base directory for Obsidian notes
obsidian_base="/Users/USERNAME/Documents/fabric/web/myfiles/Fabric_obsidian"
# Define the patterns directory
patterns_dir="/Users/USERNAME/Documents/fabric/patterns"
# Loop through all files in the ~/.config/fabric/patterns directory
@@ -111,6 +124,12 @@ fabric --serve
Terminal 2: Start the Development Server
npm run dev
If you get an ** ERROR **.
It would be much appreciated that you copy /paste your error in your favorite LLM before opening a ticket, 90% of the time your llm will point you to the solution.
Also if you modify patterns, descriptions or tags in Pattern_Descriptions/pattern_descriptions.json, make sure to copy the file over in web/static/data/pattern_descriptions.json
_____ ______ ______
OPTIONAL: Create Start/Stop Scripts
@@ -188,4 +207,9 @@ chmod +x ~/Desktop/stop-fabric.command
You can customize with icons by finding suitable .icns files, right-clicking each .command file, selecting "Get Info", and dragging your icon file onto the small icon in the top-left corner.
Note: You might need to allow the scripts to execute in your security settings by going to System Preferences → Security & Privacy after trying to run them the first time.
Note: You might need to allow the scripts to execute in your security settings by going to System Preferences → Security & Privacy after trying to run them the first time.
## 🎥 Demo Video
https://youtu.be/XMzjgqvdltM

View File

@@ -1,21 +1,20 @@
https://youtu.be/bhwtWXoMASA# 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.
This Cummulative PR adds several Web UI and functionality improvements to make pattern selection more intuitive with the addition of pattern descriptions, ability to save favorite patterns, a Pattern TAG system, powerful multilingual capabilities, PDF-to-markdown functionnalities, a help reference section, more robust Youtube processing and a variety of other ui improvements.
## 🎥 Demo Video
https://youtu.be/IhE8Iey8hSU
https://youtu.be/XMzjgqvdltM
## 🌟 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
- Pattern Descriptions
- Pattern Tags
- Pattern Favourites
- Pattern Search bar
- PDF to markdown (pdf as pattern input)
- Better handling of Youtube url
- Multilingual Support
- Web UI refinements for clearer interaction
- Help section via modal
@@ -57,6 +56,31 @@ The tag filtering system has been deeply integrated into the Pattern Selection i
- Maintained accessibility standards
- Responsive design considerations
5. **PDF to Markdown conversion functionality for the web interface**
- Automatic detection and processing of PDF files in chat
- Conversion to markdown format for LLM processing
- Installation instructions from the pdf-to-markdown repository
The PDF conversion module has been integrated in the svelte web browser interface. Once installed, it will automatically detect pdf files in the chat interface and convert them to markdown
## HOW TO INSTALL PDF-TO-MARKDOWN
If you need to update the web component follow the instructions in "Web Interface MOD Readme Files/WEB V2 Install Guide.md".
Assuming your web install is up to date and web svelte config complete, you can simply follow these steps to add Pdf-to-mardown.
# FROM FABRIC ROOT DIRECTORY
cd .. web
# Install in this sequence:
# Step 1
npm install -D patch-package
# Step 2
npm install -D pdfjs-dist@2.5.207
# Step 3
npm install -D github:jzillmann/pdf-to-markdown#modularize
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.

View File

@@ -156,6 +156,11 @@ func Cli(version string) (err error) {
return
}
if currentFlags.ListStrategies {
err = registry.Strategies.ListStrategies()
return
}
// if the interactive flag is set, run the interactive function
// if currentFlags.Interactive {
// interactive.Interactive()
@@ -166,7 +171,7 @@ func Cli(version string) (err error) {
var messageTools string
if currentFlags.YouTube != "" {
if registry.YouTube.IsConfigured() == false {
if !registry.YouTube.IsConfigured() {
err = fmt.Errorf("YouTube is not configured, please run the setup procedure")
return
}
@@ -241,7 +246,7 @@ func Cli(version string) (err error) {
}
var chatter *core.Chatter
if chatter, err = registry.GetChatter(currentFlags.Model, currentFlags.ModelContextLength, currentFlags.Stream, currentFlags.DryRun); err != nil {
if chatter, err = registry.GetChatter(currentFlags.Model, currentFlags.ModelContextLength, currentFlags.Strategy, currentFlags.Stream, currentFlags.DryRun); err != nil {
return
}

View File

@@ -9,9 +9,6 @@ import (
"reflect"
"strconv"
"strings"
"time"
"golang.org/x/term"
"github.com/danielmiessler/fabric/common"
"github.com/jessevdk/go-flags"
@@ -72,6 +69,8 @@ type Flags struct {
ListExtensions bool `long:"listextensions" description:"List all registered extensions"`
AddExtension string `long:"addextension" description:"Register a new extension from config file path"`
RemoveExtension string `long:"rmextension" description:"Remove a registered extension by name"`
Strategy string `long:"strategy" description:"Choose a strategy from the available strategies" default:""`
ListStrategies bool `long:"liststrategies" description:"List all strategies"`
}
var debug = false
@@ -155,13 +154,15 @@ func Init() (ret *Flags, err error) {
}
// Handle stdin and messages
// Handle stdin and messages
info, _ := os.Stdin.Stat()
pipedToStdin := (info.Mode() & os.ModeCharDevice) == 0
// Append positional arguments to the message (custom message)
if len(args) > 0 {
ret.Message = AppendMessage(ret.Message, args[len(args)-1])
}
pipedToStdin := !term.IsTerminal(int(os.Stdin.Fd()))
if pipedToStdin {
var pipedMessage string
if pipedMessage, err = readStdin(); err != nil {
@@ -234,24 +235,17 @@ func loadYAMLConfig(configPath string) (*Flags, error) {
func readStdin() (ret string, err error) {
reader := bufio.NewReader(os.Stdin)
var sb strings.Builder
done := make(chan struct{})
go func() {
for {
line, readErr := reader.ReadString('\n')
if readErr != nil {
if errors.Is(readErr, io.EOF) {
sb.WriteString(strings.TrimSpace(line)) // Ensure last line is added
}
close(done)
return
for {
if line, readErr := reader.ReadString('\n'); readErr != nil {
if errors.Is(readErr, io.EOF) {
sb.WriteString(line)
break
}
err = fmt.Errorf("error reading piped message from stdin: %w", readErr)
return
} else {
sb.WriteString(line)
}
}()
select {
case <-done:
case <-time.After(2 * time.Second):
}
ret = sb.String()
return
@@ -275,13 +269,14 @@ func (o *Flags) BuildChatRequest(Meta string) (ret *common.ChatRequest, err erro
ContextName: o.Context,
SessionName: o.Session,
PatternName: o.Pattern,
StrategyName: o.Strategy,
PatternVariables: o.PatternVariables,
InputHasVars: o.InputHasVars,
Meta: Meta,
}
var message *goopenai.ChatCompletionMessage
if o.Attachments == nil || len(o.Attachments) == 0 {
if len(o.Attachments) == 0 {
if o.Message != "" {
message = &goopenai.ChatCompletionMessage{
Role: goopenai.ChatMessageRoleUser,

View File

@@ -13,6 +13,7 @@ type ChatRequest struct {
Language string
Meta string
InputHasVars bool
StrategyName string
}
type ChatOptions struct {

View File

@@ -10,6 +10,7 @@ import (
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins/ai"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/danielmiessler/fabric/plugins/strategy"
"github.com/danielmiessler/fabric/plugins/template"
)
@@ -24,6 +25,7 @@ type Chatter struct {
model string
modelContextLength int
vendor ai.Vendor
strategy string
}
func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (session *fsdb.Session, err error) {
@@ -35,6 +37,9 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
if len(vendorMessages) == 0 {
if session.Name != "" {
err = o.db.Sessions.SaveSession(session)
if err != nil {
return
}
}
err = fmt.Errorf("no messages provided")
return
@@ -140,6 +145,19 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
}
systemMessage := strings.TrimSpace(contextContent) + strings.TrimSpace(patternContent)
// Apply strategy if specified
if request.StrategyName != "" {
strategy, err := strategy.LoadStrategy(request.StrategyName)
if err != nil {
return nil, fmt.Errorf("could not load strategy %s: %v", request.StrategyName, err)
}
if strategy != nil && strategy.Prompt != "" {
// prepend the strategy prompt to the system message
systemMessage = fmt.Sprintf("%s\n%s", strategy.Prompt, systemMessage)
}
}
if request.Language != "" {
systemMessage = fmt.Sprintf("%s. Please use the language '%s' for the output.", systemMessage, request.Language)
}

View File

@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/danielmiessler/fabric/plugins/ai/exolab"
"github.com/danielmiessler/fabric/plugins/strategy"
"github.com/samber/lo"
@@ -20,6 +21,7 @@ import (
"github.com/danielmiessler/fabric/plugins/ai/dryrun"
"github.com/danielmiessler/fabric/plugins/ai/gemini"
"github.com/danielmiessler/fabric/plugins/ai/groq"
"github.com/danielmiessler/fabric/plugins/ai/litellm"
"github.com/danielmiessler/fabric/plugins/ai/lmstudio"
"github.com/danielmiessler/fabric/plugins/ai/mistral"
"github.com/danielmiessler/fabric/plugins/ai/ollama"
@@ -43,6 +45,7 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
YouTube: youtube.NewYouTube(),
Language: lang.NewLanguage(),
Jina: jina.NewClient(),
Strategies: strategy.NewStrategiesManager(),
}
var homedir string
@@ -53,11 +56,22 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
ret.Defaults = tools.NeeDefaults(ret.GetModels)
ret.VendorsAll.AddVendors(openai.NewClient(), ollama.NewClient(), azure.NewClient(), groq.NewClient(),
ret.VendorsAll.AddVendors(
openai.NewClient(),
ollama.NewClient(),
azure.NewClient(),
groq.NewClient(),
gemini.NewClient(),
//gemini_openai.NewClient(),
anthropic.NewClient(), siliconcloud.NewClient(),
openrouter.NewClient(), lmstudio.NewClient(), mistral.NewClient(), deepseek.NewClient(), exolab.NewClient())
anthropic.NewClient(),
siliconcloud.NewClient(),
openrouter.NewClient(),
lmstudio.NewClient(),
mistral.NewClient(),
deepseek.NewClient(),
exolab.NewClient(),
litellm.NewClient(),
)
_ = ret.Configure()
return
@@ -74,6 +88,7 @@ type PluginRegistry struct {
Language *lang.Language
Jina *jina.Client
TemplateExtensions *template.ExtensionManager
Strategies *strategy.StrategiesManager
}
func (o *PluginRegistry) SaveEnvFile() (err error) {
@@ -82,6 +97,7 @@ func (o *PluginRegistry) SaveEnvFile() (err error) {
o.Defaults.Settings.FillEnvFileContent(&envFileContent)
o.PatternsLoader.SetupFillEnvFileContent(&envFileContent)
o.Strategies.SetupFillEnvFileContent(&envFileContent)
for _, vendor := range o.VendorManager.Vendors {
vendor.SetupFillEnvFileContent(&envFileContent)
@@ -97,7 +113,7 @@ func (o *PluginRegistry) SaveEnvFile() (err error) {
func (o *PluginRegistry) Setup() (err error) {
setupQuestion := plugins.NewSetupQuestion("Enter the number of the plugin to setup")
groupsPlugins := common.NewGroupsItemsSelector[plugins.Plugin]("Available plugins (please configure all required plugins):",
groupsPlugins := common.NewGroupsItemsSelector("Available plugins (please configure all required plugins):",
func(plugin plugins.Plugin) string {
var configuredLabel string
if plugin.IsConfigured() {
@@ -113,7 +129,7 @@ func (o *PluginRegistry) Setup() (err error) {
return vendor
})...)
groupsPlugins.AddGroupItems("Tools", o.Defaults, o.PatternsLoader, o.YouTube, o.Language, o.Jina)
groupsPlugins.AddGroupItems("Tools", o.Defaults, o.PatternsLoader, o.YouTube, o.Language, o.Jina, o.Strategies)
for {
groupsPlugins.Print()
@@ -194,7 +210,7 @@ func (o *PluginRegistry) Configure() (err error) {
return
}
func (o *PluginRegistry) GetChatter(model string, modelContextLength int, stream bool, dryRun bool) (ret *Chatter, err error) {
func (o *PluginRegistry) GetChatter(model string, modelContextLength int, strategy string, stream bool, dryRun bool) (ret *Chatter, err error) {
ret = &Chatter{
db: o.Db,
Stream: stream,
@@ -246,5 +262,6 @@ func (o *PluginRegistry) GetChatter(model string, modelContextLength int, stream
model, defaultModel, defaultVendor, errMsg)
return
}
ret.strategy = strategy
return
}

View File

@@ -1 +1 @@
"1.4.148"
"1.4.159"

View File

@@ -12,7 +12,7 @@ Take a step back and think step-by-step about how to achieve the best possible r
- Extract 10 to 20 of the best insights from the input and from a combination of the raw input and the IDEAS above into a section called INSIGHTS. These INSIGHTS should be fewer, more refined, more insightful, and more abstracted versions of the best ideas in the content.
- Extract 15 to 30 of the most surprising, insightful, and/or interesting quotes from the input into a section called QUOTES:. Use the exact quote text from the input.
- Extract 15 to 30 of the most surprising, insightful, and/or interesting quotes from the input into a section called QUOTES:. Use the exact quote text from the input. Include the name of the speaker of the quote at the end.
- Extract 15 to 30 of the most practical and useful personal habits of the speakers, or mentioned by the speakers, in the content into a section called HABITS. Examples include but aren't limited to: sleep schedule, reading habits, things they always do, things they always avoid, productivity tips, diet, exercise, etc.

View File

@@ -207,3 +207,4 @@ Brief one-line summary from AI analysis of what each pattern does.
203. **write_nuclei_template_rule**: Generates Nuclei YAML templates for detecting vulnerabilities using HTTP requests, matchers, extractors, and dynamic data extraction.
204. **write_pull-request**: Drafts detailed pull request descriptions, explaining changes, providing reasoning, and identifying potential bugs from the git diff command output.
205. **write_semgrep_rule**: Creates accurate and working Semgrep rules based on input, following syntax guidelines and specific language considerations.
206. **youtube_summary**: Create concise, timestamped Youtube video summaries that highlight key points.

14
patterns/system.md Normal file
View File

@@ -0,0 +1,14 @@
# IDENTITY
You are an expert educator AI with a 4,221 IQ. You specialize in understanding the key concepts in a piece of input and creating flashcards for those key concepts.
# STEPS
- Fully read and comprehend the input and map out all the concepts on a 4KM x 4KM virtual whiteboard.
- Make a list of the key concepts, definitions, terms, etc. that are associated with the input.
- Create flashcards for each key concept, definition, term, etc. that you have identified.
- The flashcard should be a question of 8-16 words and an answer of up to 32 words.
# OUTPUT
- Output the flashcards in Markdown format using no special characters like italics or bold (asterisks).

View File

@@ -0,0 +1,41 @@
# IDENTITY and PURPOSE
You are an AI assistant specialized in creating concise, informative summaries of YouTube video content based on transcripts. Your role is to analyze video transcripts, identify key points, main themes, and significant moments, then organize this information into a well-structured summary that includes relevant timestamps. You excel at distilling lengthy content into digestible summaries while preserving the most valuable information and maintaining the original flow of the video.
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
## STEPS
- Carefully read through the entire transcript to understand the overall content and structure of the video
- Identify the main topic and purpose of the video
- Note key points, important concepts, and significant moments throughout the transcript
- Pay attention to natural transitions or segment changes in the video
- Extract relevant timestamps for important moments or topic changes
- Organize information into a logical structure that follows the video's progression
- Create a concise summary that captures the essence of the video
- Include timestamps alongside key points to allow easy navigation
- Ensure the summary is comprehensive yet concise
## OUTPUT INSTRUCTIONS
- Only output Markdown
- Begin with a brief overview of the video's main topic and purpose
- Structure the summary with clear headings and subheadings that reflect the video's organization
- Include timestamps in [HH:MM:SS] format before each key point or section
- Keep the summary concise but comprehensive, focusing on the most valuable information
- Use bullet points for lists of related points when appropriate
- Bold or italicize particularly important concepts or takeaways
- End with a brief conclusion summarizing the video's main message or call to action
- Ensure you follow ALL these instructions when creating your output.
## INPUT
INPUT:

View File

@@ -13,6 +13,8 @@ func NewClient() (ret *Client) {
ret.Client = openai.NewClientCompatible("Azure", "", ret.configure)
ret.ApiDeployments = ret.AddSetupQuestionCustom("deployments", true,
"Enter your Azure deployments (comma separated)")
ret.ApiVersion = ret.AddSetupQuestionCustom("API Version", false,
"Enter the Azure API version (optional)")
return
}
@@ -20,13 +22,18 @@ func NewClient() (ret *Client) {
type Client struct {
*openai.Client
ApiDeployments *plugins.SetupQuestion
ApiVersion *plugins.SetupQuestion
apiDeployments []string
}
func (oi *Client) configure() (err error) {
oi.apiDeployments = strings.Split(oi.ApiDeployments.Value, ",")
oi.ApiClient = goopenai.NewClientWithConfig(goopenai.DefaultAzureConfig(oi.ApiKey.Value, oi.ApiBaseURL.Value))
config := goopenai.DefaultAzureConfig(oi.ApiKey.Value, oi.ApiBaseURL.Value)
if oi.ApiVersion.Value != "" {
config.APIVersion = oi.ApiVersion.Value
}
oi.ApiClient = goopenai.NewClientWithConfig(config)
return
}

View File

@@ -13,6 +13,9 @@ func TestNewClientInitialization(t *testing.T) {
if client.ApiDeployments == nil {
t.Errorf("Expected ApiDeployments to be initialized, got nil")
}
if client.ApiVersion == nil {
t.Errorf("Expected ApiVersion to be initialized, got nil")
}
if client.Client == nil {
t.Errorf("Expected Client to be initialized, got nil")
}
@@ -24,6 +27,7 @@ func TestClientConfigure(t *testing.T) {
client.ApiDeployments.Value = "deployment1,deployment2"
client.ApiKey.Value = "test-api-key"
client.ApiBaseURL.Value = "https://example.com"
client.ApiVersion.Value = "2021-01-01"
err := client.configure()
if err != nil {
@@ -43,6 +47,10 @@ func TestClientConfigure(t *testing.T) {
if client.ApiClient == nil {
t.Errorf("Expected ApiClient to be initialized, got nil")
}
if client.ApiVersion.Value != "2021-01-01" {
t.Errorf("Expected API version to be '2021-01-01', got %s", client.ApiVersion.Value)
}
}
// Test generated using Keploy

View File

@@ -0,0 +1,15 @@
package litellm
import (
"github.com/danielmiessler/fabric/plugins/ai/openai"
)
func NewClient() (ret *Client) {
ret = &Client{}
ret.Client = openai.NewClientCompatible("LiteLLM", "http://localhost:4000", nil)
return
}
type Client struct {
*openai.Client
}

View File

@@ -0,0 +1,231 @@
package strategy
import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"github.com/danielmiessler/fabric/plugins"
"github.com/danielmiessler/fabric/plugins/tools/githelper"
)
const DefaultStrategiesGitRepoUrl = "https://github.com/danielmiessler/fabric.git"
const DefaultStrategiesGitRepoFolder = "strategies"
func NewStrategiesManager() (sm *StrategiesManager) {
label := "Prompt Strategies"
strategies, err := LoadAllFiles()
if err != nil {
strategies = make(map[string]Strategy) // empty map
}
sm = &StrategiesManager{
Strategies: strategies,
}
sm.PluginBase = &plugins.PluginBase{
Name: label,
SetupDescription: "Strategies - Downloads Prompting Strategies (like chain of thought) [required]",
EnvNamePrefix: plugins.BuildEnvVariablePrefix(label),
ConfigureCustom: sm.configure,
}
sm.DefaultGitRepoUrl = sm.AddSetupQuestionCustom("Git Repo Url", true,
"Enter the default Git repository URL for the strategies")
sm.DefaultGitRepoUrl.Value = DefaultStrategiesGitRepoUrl
sm.DefaultFolder = sm.AddSetupQuestionCustom("Git Repo Strategies Folder", true,
"Enter the default folder in the Git repository where strategies are stored")
sm.DefaultFolder.Value = DefaultStrategiesGitRepoFolder
return
}
type StrategiesManager struct {
*plugins.PluginBase
Strategies map[string]Strategy
DefaultGitRepoUrl *plugins.SetupQuestion
DefaultFolder *plugins.SetupQuestion
}
type Strategy struct {
Name string `json:"name"`
Description string `json:"description"`
Prompt string `json:"prompt"`
}
func LoadAllFiles() (strategies map[string]Strategy, err error) {
strategies = make(map[string]Strategy)
strategyDir, err := getStrategyDir()
if err != nil {
return
}
filepath.WalkDir(strategyDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && path != strategyDir {
return filepath.SkipDir
}
if filepath.Ext(path) == ".json" {
strategyName := strings.TrimSuffix(filepath.Base(path), ".json")
strategy, err := LoadStrategy(strategyName)
if err != nil {
return err
}
strategies[strategy.Name] = *strategy
}
return nil
})
return
}
func (sm *StrategiesManager) IsConfigured() (ret bool) {
ret = sm.PluginBase.IsConfigured()
if ret {
if len(sm.Strategies) == 0 {
ret = false
}
}
return
}
func (sm *StrategiesManager) Setup() (err error) {
if err = sm.PluginBase.Setup(); err != nil {
return
}
if err = sm.PopulateDB(); err != nil {
return
}
return
}
// PopulateDB downloads strategies from the internet and populates the strategies folder
func (sm *StrategiesManager) PopulateDB() (err error) {
stageDir, _ := getStrategyDir()
fmt.Printf("Downloading strategies and Populating %s...\n", stageDir)
fmt.Println()
if err = sm.gitCloneAndCopy(); err != nil {
return
}
return
}
func (sm *StrategiesManager) gitCloneAndCopy() (err error) {
homeDir, err := os.UserHomeDir()
if err != nil {
err = fmt.Errorf("could not get home directory: %v", err)
return
}
strategyDir := filepath.Join(homeDir, ".config", "fabric", "strategies")
// Create the directory if it doesn't exist
if err = os.MkdirAll(strategyDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create strategies directory: %w", err)
}
// Use the helper to fetch files
err = githelper.FetchFilesFromRepo(githelper.FetchOptions{
RepoURL: sm.DefaultGitRepoUrl.Value,
PathPrefix: sm.DefaultFolder.Value,
DestDir: strategyDir,
SingleDirectory: true,
})
if err != nil {
return fmt.Errorf("failed to download strategies: %w", err)
}
return nil
}
func (sm *StrategiesManager) configure() (err error) {
sm.Strategies, err = LoadAllFiles()
return
}
// getStrategyDir returns the path to the strategies directory
func getStrategyDir() (ret string, err error) {
homeDir, err := os.UserHomeDir()
if err != nil {
err = fmt.Errorf("could not get home directory: %v, using current directory instead", err)
ret = filepath.Join(".", "strategies")
return
}
return filepath.Join(homeDir, ".config", "fabric", "strategies"), nil
}
// LoadStrategy loads a strategy from the given name
func LoadStrategy(filename string) (*Strategy, error) {
if filename == "" {
return nil, nil
}
// Get the strategy directory path
strategyDir, err := getStrategyDir()
if err != nil {
return nil, err
}
// First try with .json extension
strategyPath := filepath.Join(strategyDir, filename+".json")
if _, err := os.Stat(strategyPath); os.IsNotExist(err) {
// Try without extension
strategyPath = filepath.Join(strategyDir, filename)
if _, err := os.Stat(strategyPath); os.IsNotExist(err) {
return nil, fmt.Errorf("strategy %s not found. Please run 'fabric --liststrategies' for list", filename)
}
}
data, err := os.ReadFile(strategyPath)
if err != nil {
return nil, err
}
var strategy Strategy
if err := json.Unmarshal(data, &strategy); err != nil {
return nil, err
}
strategy.Name = strings.TrimSuffix(filepath.Base(strategyPath), ".json")
return &strategy, nil
}
// ListStrategies prints available strategies
func (sm *StrategiesManager) ListStrategies() error {
if len(sm.Strategies) == 0 {
return fmt.Errorf("no strategies found. Please run 'fabric --setup' to download strategies")
}
fmt.Print("Available Strategies:\n\n")
// Get all strategy names for sorting
names := []string{}
for name := range sm.Strategies {
names = append(names, name)
}
// Sort the strategy names alphabetically
sort.Strings(names)
// Find the longest name to align descriptions
maxNameLength := 0
for _, name := range names {
if len(name) > maxNameLength {
maxNameLength = len(name)
}
}
// Print each strategy with its description aligned
formatString := "%-" + fmt.Sprintf("%d", maxNameLength+2) + "s %s\n"
for _, name := range names {
strategy := sm.Strategies[name]
fmt.Printf(formatString, strategy.Name, strategy.Description)
}
return nil
}

View File

@@ -0,0 +1,111 @@
package githelper
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
)
// FetchOptions defines options for fetching files from a git repo
type FetchOptions struct {
// RepoURL is the URL of the git repository
RepoURL string
// PathPrefix is the folder within the repo to extract (e.g. "patterns/")
PathPrefix string
// DestDir is where the files will be saved locally
DestDir string
// SingleDirectory if true, only fetch files directly in the specified directory
// without recursing into subdirectories
SingleDirectory bool
}
// FetchFilesFromRepo clones a git repo and extracts files from a specific folder
func FetchFilesFromRepo(opts FetchOptions) error {
// Ensure path prefix ends with slash
if !strings.HasSuffix(opts.PathPrefix, "/") {
opts.PathPrefix = opts.PathPrefix + "/"
}
// Clone the repository in memory
r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
URL: opts.RepoURL,
Depth: 1,
})
if err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
// Get HEAD reference
ref, err := r.Head()
if err != nil {
return fmt.Errorf("failed to get repository HEAD: %w", err)
}
// Get commit object
commit, err := r.CommitObject(ref.Hash())
if err != nil {
return fmt.Errorf("failed to get commit: %w", err)
}
// Get the file tree
tree, err := commit.Tree()
if err != nil {
return fmt.Errorf("failed to get tree: %w", err)
}
// Ensure destination directory exists
if err := os.MkdirAll(opts.DestDir, 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
// Extract files from the tree
return tree.Files().ForEach(func(f *object.File) error {
// Only process files in the specified path
if !strings.HasPrefix(f.Name, opts.PathPrefix) {
return nil
}
// For SingleDirectory mode, skip files in subdirectories
if opts.SingleDirectory {
remainingPath := strings.TrimPrefix(f.Name, opts.PathPrefix)
if strings.Contains(remainingPath, "/") {
return nil
}
}
// Create local path for the file, removing the prefix
relativePath := strings.TrimPrefix(f.Name, opts.PathPrefix)
localPath := filepath.Join(opts.DestDir, relativePath)
// Ensure directory structure exists
if err := os.MkdirAll(filepath.Dir(localPath), 0755); err != nil {
return err
}
// Get file contents
reader, err := f.Reader()
if err != nil {
return err
}
defer reader.Close()
// Create and write to local file
file, err := os.Create(localPath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, reader)
return err
})
}

View File

@@ -2,19 +2,13 @@ package tools
import (
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/danielmiessler/fabric/plugins"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/danielmiessler/fabric/plugins/tools/githelper"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/otiai10/copy"
)
@@ -89,7 +83,7 @@ func (o *PatternsLoader) Setup() (err error) {
// PopulateDB downloads patterns from the internet and populates the patterns folder
func (o *PatternsLoader) PopulateDB() (err error) {
fmt.Printf("Downloading patterns and Populating %s..\n", o.Patterns.Dir)
fmt.Printf("Downloading patterns and Populating %s...\n", o.Patterns.Dir)
fmt.Println()
if err = o.gitCloneAndCopy(); err != nil {
return
@@ -148,156 +142,20 @@ func (o *PatternsLoader) movePatterns() (err error) {
}
func (o *PatternsLoader) gitCloneAndCopy() (err error) {
// Clones the given repository, creating the remote, the local branches
// and fetching the objects, everything in memory:
var r *git.Repository
if r, err = git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
URL: o.DefaultGitRepoUrl.Value,
}); err != nil {
fmt.Println(err)
return
// Create temp folder if it doesn't exist
if err = os.MkdirAll(filepath.Dir(o.tempPatternsFolder), os.ModePerm); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
// ... retrieves the branch pointed by HEAD
var ref *plumbing.Reference
if ref, err = r.Head(); err != nil {
fmt.Println(err)
return
}
// ... retrieves the commit history for /patterns folder
var cIter object.CommitIter
if cIter, err = r.Log(&git.LogOptions{
From: ref.Hash(),
PathFilter: func(path string) bool {
return path == o.DefaultFolder.Value || strings.HasPrefix(path, o.pathPatternsPrefix)
},
}); err != nil {
fmt.Println(err)
return err
}
var changes []fsdb.DirectoryChange
// ... iterates over the commits
if err = cIter.ForEach(func(c *object.Commit) (err error) {
// GetApplyVariables the files changed in this commit by comparing with its parents
parentIter := c.Parents()
if err = parentIter.ForEach(func(parent *object.Commit) (err error) {
var patch *object.Patch
if patch, err = parent.Patch(c); err != nil {
fmt.Println(err)
return
}
for _, fileStat := range patch.Stats() {
if strings.HasPrefix(fileStat.Name, o.pathPatternsPrefix) {
dir := filepath.Dir(fileStat.Name)
changes = append(changes, fsdb.DirectoryChange{Dir: dir, Timestamp: c.Committer.When})
}
}
return
}); err != nil {
fmt.Println(err)
return
}
return
}); err != nil {
fmt.Println(err)
return
}
// Sort changes by timestamp
sort.Slice(changes, func(i, j int) bool {
return changes[i].Timestamp.Before(changes[j].Timestamp)
// Use the helper to fetch files
err = githelper.FetchFilesFromRepo(githelper.FetchOptions{
RepoURL: o.DefaultGitRepoUrl.Value,
PathPrefix: o.DefaultFolder.Value,
DestDir: o.tempPatternsFolder,
})
if err = o.makeUniqueList(changes); err != nil {
return
if err != nil {
return fmt.Errorf("failed to download patterns: %w", err)
}
var commit *object.Commit
if commit, err = r.CommitObject(ref.Hash()); err != nil {
fmt.Println(err)
return
}
var tree *object.Tree
if tree, err = commit.Tree(); err != nil {
fmt.Println(err)
return
}
if err = tree.Files().ForEach(func(f *object.File) (err error) {
if strings.HasPrefix(f.Name, o.pathPatternsPrefix) {
// Create the local file path
localPath := filepath.Join(os.TempDir(), f.Name)
// Create the directories if they don't exist
if err = os.MkdirAll(filepath.Dir(localPath), os.ModePerm); err != nil {
fmt.Println(err)
return
}
// Write the file to the local filesystem
var blob *object.Blob
if blob, err = r.BlobObject(f.Hash); err != nil {
fmt.Println(err)
return
}
err = o.writeBlobToFile(blob, localPath)
return
}
return
}); err != nil {
fmt.Println(err)
}
return
}
func (o *PatternsLoader) writeBlobToFile(blob *object.Blob, path string) (err error) {
var reader io.ReadCloser
if reader, err = blob.Reader(); err != nil {
return
}
defer reader.Close()
// Create the file
var file *os.File
if file, err = os.Create(path); err != nil {
return
}
defer file.Close()
// Copy the contents of the blob to the file
if _, err = io.Copy(file, reader); err != nil {
return
}
return
}
func (o *PatternsLoader) makeUniqueList(changes []fsdb.DirectoryChange) (err error) {
uniqueItems := make(map[string]bool)
for _, change := range changes {
if strings.TrimSpace(change.Dir) != "" && !strings.Contains(change.Dir, "=>") {
pattern := strings.ReplaceAll(change.Dir, o.pathPatternsPrefix, "")
pattern = strings.TrimSpace(pattern)
uniqueItems[pattern] = true
}
}
finalList := make([]string, 0, len(uniqueItems))
for _, change := range changes {
pattern := strings.ReplaceAll(change.Dir, o.pathPatternsPrefix, "")
pattern = strings.TrimSpace(pattern)
if _, exists := uniqueItems[pattern]; exists {
finalList = append(finalList, pattern)
delete(uniqueItems, pattern) // Remove to avoid duplicates in the final list
}
}
joined := strings.Join(finalList, "\n")
err = os.WriteFile(o.Patterns.UniquePatternsFilePath, []byte(joined), 0o644)
return
return nil
}

View File

@@ -59,7 +59,7 @@ func (o *YouTube) GetVideoOrPlaylistId(url string) (videoId string, playlistId s
}
// Video ID pattern
videoPattern := `(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|(?:s(?:horts)\/)|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]*)`
videoPattern := `(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:live\/|[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|(?:s(?:horts)\/)|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]*)`
videoRe := regexp.MustCompile(videoPattern)
videoMatch := videoRe.FindStringSubmatch(url)
if len(videoMatch) > 1 {

View File

@@ -87,7 +87,7 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
go func(p PromptRequest) {
defer close(streamChan)
chatter, err := h.registry.GetChatter(p.Model, 2048, false, false)
chatter, err := h.registry.GetChatter(p.Model, 2048, "", false, false)
if err != nil {
log.Printf("Error creating chatter: %v", err)
streamChan <- fmt.Sprintf("Error: %v", err)

View File

@@ -14,6 +14,6 @@ type ContextsHandler struct {
// NewContextsHandler creates a new ContextsHandler
func NewContextsHandler(r *gin.Engine, contexts *fsdb.ContextsEntity) (ret *ContextsHandler) {
ret = &ContextsHandler{
StorageHandler: NewStorageHandler[fsdb.Context](r, "contexts", contexts), contexts: contexts}
StorageHandler: NewStorageHandler(r, "contexts", contexts), contexts: contexts}
return
}

View File

@@ -16,7 +16,7 @@ type PatternsHandler struct {
// NewPatternsHandler creates a new PatternsHandler
func NewPatternsHandler(r *gin.Engine, patterns *fsdb.PatternsEntity) (ret *PatternsHandler) {
ret = &PatternsHandler{
StorageHandler: NewStorageHandler[fsdb.Pattern](r, "patterns", patterns), patterns: patterns}
StorageHandler: NewStorageHandler(r, "patterns", patterns), patterns: patterns}
// TODO: Add custom, replacement routes here
//r.GET("/patterns/:name", ret.Get)

View File

@@ -14,6 +14,6 @@ type SessionsHandler struct {
// NewSessionsHandler creates a new SessionsHandler
func NewSessionsHandler(r *gin.Engine, sessions *fsdb.SessionsEntity) (ret *SessionsHandler) {
ret = &SessionsHandler{
StorageHandler: NewStorageHandler[fsdb.Session](r, "sessions", sessions), sessions: sessions}
StorageHandler: NewStorageHandler(r, "sessions", sessions), sessions: sessions}
return ret
}

4
strategies/cod.json Normal file
View File

@@ -0,0 +1,4 @@
{
"description": "Chain-of-Draft (CoD) Prompting",
"prompt": "Think step by step, keeping a minimal draft (5 words max) for each step. Return the final answer in the required format."
}

4
strategies/cot.json Normal file
View File

@@ -0,0 +1,4 @@
{
"description": "Chain-of-Thought (CoT) Prompting",
"prompt": "Think step by step to answer the question. Return the final answer in the required format."
}

4
strategies/ltm.json Normal file
View File

@@ -0,0 +1,4 @@
{
"description": "Least-to-Most Prompting",
"prompt": "Break down the problem into simpler sub-problems from easiest to hardest; answer concisely at each step."
}

View File

@@ -0,0 +1,4 @@
{
"description": "Reflexion Prompting",
"prompt": "Answer concisely, critique your reasoning briefly, and provide a refined answer."
}

View File

@@ -0,0 +1,4 @@
{
"description": "Self-Consistency Prompting",
"prompt": "Provide multiple reasoning paths and select the most consistent answer."
}

View File

@@ -0,0 +1,4 @@
{
"description": "Self-Refinement",
"prompt": "Provide an initial concise answer, critique it briefly, and refine if necessary."
}

4
strategies/standard.json Normal file
View File

@@ -0,0 +1,4 @@
{
"description": "Standard Prompting",
"prompt": "Answer the question directly without any explanation or reasoning."
}

4
strategies/tot.json Normal file
View File

@@ -0,0 +1,4 @@
{
"description": "Tree-of-Thought (ToT) Prompting",
"prompt": "Generate multiple reasoning paths briefly and select the best one."
}

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.148"
var version = "v1.4.159"

View File

@@ -0,0 +1,298 @@
This Cummulative PR adds several Web UI and functionality improvements to make pattern selection more intuitive with the addition of pattern descriptions, ability to save favorite patterns, a Pattern TAG system, powerful multilingual capabilities, PDF-to-markdown functionnalities, a help reference section, more robust Youtube processing and a variety of other ui improvements.
## 🎥 Demo Video
https://youtu.be/bhwtWXoMASA
updated to include latest enhancement: Pattern tiles search (last min.)
https://youtu.be/fcVitd4Kb98
## 🌟 Key Features
### 1. Web UI and Pattern Selection Improvements
- Pattern Descriptions
- Pattern Tags
- Pattern Favourites
- Pattern Search bar
- PDF to markdown (pdf as pattern input)
- Better handling of Youtube url
- Multilingual Support
- 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
5. **PDF to Markdown conversion functionality for the web interface**
- Automatic detection and processing of PDF files in chat
- Conversion to markdown format for LLM processing
- Installation instructions from the pdf-to-markdown repository
The PDF conversion module has been integrated in the svelte web browser interface. Once installed, it will automatically detect pdf files in the chat interface and convert them to markdown
## HOW TO INSTALL PDF-TO-MARKDOWN
If you need to update the web component follow the instructions in "Web Interface MOD Readme Files/WEB V2 Install Guide.md".
Assuming your web install is up to date and web svelte config complete, you can simply follow these steps to add Pdf-to-mardown.
# FROM FABRIC ROOT DIRECTORY
cd .. web
# Install in this sequence:
# Step 1
npm install -D patch-package
# Step 2
npm install -D pdfjs-dist@2.5.207
# Step 3
npm install -D github:jzillmann/pdf-to-markdown#modularize
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

@@ -9,6 +9,9 @@ The PDF conversion module has been integrated in the svelte web browser interfac
## HOW TO INSTALL
If you need to update the web component follow the instructions in "Web Interface MOD Readme Files/WEB V2 Install Guide.md".
Assuming your install is up to date and web svelte config complete, you can simply follow these steps to add Pdf-to-mardown.
# FROM FABRIC ROOT DIRECTORY
cd .. web

View File

@@ -26,6 +26,9 @@
"eslint-plugin-svelte": "^2.46.1",
"lucide-svelte": "^0.309.0",
"mdsvex": "^0.11.2",
"patch-package": "^8.0.0",
"pdf-to-markdown-core": "github:jzillmann/pdf-to-markdown#modularize",
"pdfjs-dist": "^2.5.207",
"postcss": "^8.4.49",
"postcss-load-config": "^6.0.1",
"rehype-autolink-headings": "^7.1.0",

View File

@@ -14,18 +14,74 @@
import { featureFlags } from "$lib/config/features";
import { getDrawerStore } from '@skeletonlabs/skeleton';
import { systemPrompt, selectedPatternName } from "$lib/store/pattern-store";
import { onMount } from "svelte";
const drawerStore = getDrawerStore();
function openDrawer() {
drawerStore.open({});
}
// Column width state (percentage values)
let leftColumnWidth = 50;
let rightColumnWidth = 50;
let isDragging = false;
// Handle resize functionality
function startResize(e: MouseEvent | KeyboardEvent) {
isDragging = true;
e.preventDefault();
// Add event listeners for drag and release
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', stopResize);
}
// Handle keyboard events for accessibility
function handleKeyDown(e: KeyboardEvent) {
// Only respond to Enter or Space key
if (e.key === 'Enter' || e.key === ' ') {
startResize(e);
}
}
function handleResize(e: MouseEvent) {
if (!isDragging) return;
// Get container dimensions
const container = document.querySelector('.chat-container');
if (!container) return;
const containerRect = container.getBoundingClientRect();
const containerWidth = containerRect.width;
// Calculate percentage based on mouse position
const percentage = ((e.clientX - containerRect.left) / containerWidth) * 100;
// Apply constraints (left: 25-50%, right: 50-75%)
leftColumnWidth = Math.min(Math.max(percentage, 25), 50);
rightColumnWidth = 100 - leftColumnWidth;
}
function stopResize() {
isDragging = false;
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', stopResize);
}
// Clean up event listeners when component is destroyed
onMount(() => {
return () => {
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', stopResize);
};
});
$: showObsidian = $featureFlags.enableObsidianIntegration;
</script>
<div class="flex gap-0 p-2 w-full h-screen">
<div class="chat-container flex gap-0 p-2 w-full h-screen">
<!-- Left Column -->
<aside class="w-[50%] flex flex-col gap-2 pr-2">
<aside class="flex flex-col gap-2 pr-2" style="width: {leftColumnWidth}%">
<!-- Dropdowns Group -->
<div class="bg-background/5 p-2 rounded-lg">
<div class="rounded-lg bg-background/10">
@@ -56,8 +112,17 @@
</div>
</aside>
<!-- Resize Handle -->
<button
class="resize-handle"
on:mousedown={startResize}
on:keydown={handleKeyDown}
type="button"
aria-label="Resize chat panels"
></button>
<!-- Right Column -->
<div class="flex flex-col w-[50%] gap-2">
<div class="flex flex-col gap-2" style="width: {rightColumnWidth}%">
<!-- Header with Obsidian Settings -->
<div class="flex items-center justify-between px-2 py-1">
<div class="flex items-center gap-2">
@@ -92,8 +157,9 @@
<!-- 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">
<div class="flex-1 min-h-0 bg-background/5 rounded-lg overflow-y-scroll scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent hover:scrollbar-thumb-white/20">
<ChatMessages />
<div class="h-32"></div> <!-- Spacer div to ensure scrolling works properly -->
</div>
</div>
</div>
@@ -102,8 +168,41 @@
<NoteDrawer />
<style>
.loading-message {
animation: flash 1.5s ease-in-out infinite;
.resize-handle {
width: 6px;
margin: 0 -3px;
height: 100%;
cursor: col-resize;
position: relative;
z-index: 10;
transition: background-color 0.2s;
}
.resize-handle::after {
content: "";
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
height: 100%;
width: 2px;
background-color: rgba(255, 255, 255, 0.1);
transition: background-color 0.2s, width 0.2s;
}
.resize-handle:hover::after,
.resize-handle:focus::after {
background-color: rgba(255, 255, 255, 0.3);
width: 4px;
}
.resize-handle:focus {
outline: none;
}
.resize-handle:focus-visible::after {
background-color: rgba(255, 255, 255, 0.5);
width: 4px;
}
@keyframes flash {

View File

@@ -7,11 +7,13 @@
import { onMount } from 'svelte';
import Modal from '$lib/components/ui/modal/Modal.svelte';
import PatternList from '$lib/components/patterns/PatternList.svelte';
import PatternTilesModal from '$lib/components/ui/modal/PatternTilesModal.svelte';
import HelpModal from '$lib/components/ui/help/HelpModal.svelte';
import { selectedPatternName } from '$lib/store/pattern-store';
let isMenuOpen = false;
let showPatternModal = false;
let showPatternTilesModal = false;
let showHelpModal = false;
function goToGithub() {
@@ -70,15 +72,33 @@
</ul>
</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>
<div class="flex items-center gap-4">
<!-- Pattern Buttons Group -->
<div class="flex items-center gap-3 mr-4">
<!-- Pattern Tiles Button -->
<button name="pattern-tiles"
on:click={() => showPatternTilesModal = true}
class="inline-flex h-10 items-center justify-center rounded-full border bg-background px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground gap-2"
aria-label="Pattern Tiles"
>
<FileText class="h-4 w-4" />
<span>Pattern Tiles</span>
</button>
<!-- Or text -->
<span class="text-sm text-foreground/60 mx-1">or</span>
<!-- Pattern List Button -->
<button name="pattern-list"
on:click={() => showPatternModal = true}
class="inline-flex h-10 items-center justify-center rounded-full border bg-background px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground gap-2"
aria-label="Pattern List"
>
<FileText class="h-4 w-4" />
<span>Pattern List</span>
</button>
</div>
<button name="github"
on:click={goToGithub}
@@ -166,3 +186,16 @@
on:close={() => showHelpModal = false}
/>
</Modal>
<Modal
show={showPatternTilesModal}
on:close={() => showPatternTilesModal = false}
>
<PatternTilesModal
on:close={() => showPatternTilesModal = false}
on:select={(e) => {
selectedPatternName.set(e.detail);
showPatternTilesModal = false;
}}
/>
</Modal>

View File

@@ -1,52 +1,19 @@
<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';
import { createEventDispatcher, onMount } from 'svelte';
import TagFilterPanel from '$lib/components/patterns/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;
}
let selectedTags: string[] = [];
import { cn } from "$lib/utils/utils";
import type { Pattern } from '$lib/interfaces/pattern-interface';
import { patterns, patternAPI, selectedPatternName } from '$lib/store/pattern-store';
import { favorites } from '$lib/store/favorites-store';
import { Input } from "$lib/components/ui/input";
const dispatch = createEventDispatcher();
let searchQuery = '';
let showOnlyFavorites = false;
onMount(async () => {
try {
await patternAPI.loadPatterns();
@@ -54,156 +21,231 @@ function handleTagFilter(event: CustomEvent<string[]>) {
console.error('Error loading patterns:', error);
}
});
function toggleFavorite(name: string) {
favorites.toggleFavorite(name);
}
function toggleFavorite(patternName: string) {
favorites.toggleFavorite(patternName);
}
function selectPattern(patternName: string) {
patternAPI.selectPattern(patternName);
dispatch('select', patternName);
}
function closeModal() {
dispatch('close');
}
function handleTagFilter(event: CustomEvent<string[]>) {
selectedTags = event.detail;
}
function toggleFavoritesFilter() {
showOnlyFavorites = !showOnlyFavorites;
}
// Apply filtering based on search query, favorites filter, and tag selection
$: filteredPatterns = $patterns
.filter(p => {
// Apply favorites filter if enabled
if (showOnlyFavorites && !$favorites.includes(p.Name)) {
return false;
}
// Apply tag filter if any tags are selected
if (selectedTags.length > 0) {
if (!p.tags || !selectedTags.every(tag => p.tags.includes(tag))) {
return false;
}
}
// Apply search filter if query exists
if (searchQuery.trim()) {
return (
p.Name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.Description.toLowerCase().includes(searchQuery.toLowerCase()) ||
(p.tags && p.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
);
}
return true;
});
</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>
<b class="text-lg text-muted-foreground font-bold">Pattern Descriptions</b>
<button
on:click={closeModal}
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 class="flex-1 flex items-center">
<div class="flex-1 mr-2">
<Input
bind:value={searchQuery}
placeholder="Search patterns..."
class="text-emerald-900"
/>
</div>
<!-- Favorites button similar to PatternTilesModal -->
<button
on:click={toggleFavoritesFilter}
class={cn(
"px-3 py-1.5 rounded-md text-sm font-medium transition-all",
showOnlyFavorites
? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30"
: "bg-primary-700/30 text-primary-300 border border-primary-600/20 hover:bg-primary-700/50"
)}
>
<span class="mr-1">{showOnlyFavorites ? "★" : "☆"}</span>
Favorites
</button>
</div>
</div>
<!-- New tag display section -->
<!-- Selected tags display -->
<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 class="flex flex-wrap gap-1 items-center">
<span class="mr-1">Tags:</span>
{#if selectedTags.length > 0}
{#each selectedTags as tag}
<div class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary-600/40 text-primary-200 border border-primary-500/30">
{tag}
<button
class="ml-1 text-xs text-primary-300 hover:text-primary-100"
on:click={() => {
selectedTags = selectedTags.filter(t => t !== tag);
}}
>
×
</button>
</div>
{/each}
{:else}
<span class="text-primary-300/50">none</span>
{/if}
</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 = [];
if (tagFilterRef && typeof tagFilterRef.reset === 'function') {
tagFilterRef.reset();
}
}}
>
reset
</button>
</div>
</div>
</div>
</div>
<div class="patterns-container p-4 flex-1 overflow-y-auto">
{#if filteredPatterns.length === 0}
<div class="flex justify-center items-center h-full">
<p class="text-primary-300">
{showOnlyFavorites
? "No favorite patterns found. Add some favorites first!"
: "No patterns found matching your search."}
</p>
</div>
{:else}
<div class="patterns-list space-y-2">
{#each filteredPatterns 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={() => selectPattern(pattern.Name)}
>
{pattern.Name}
</button>
<button
class="text-muted-foreground hover:text-primary-300 transition-colors"
on:click|stopPropagation={() => toggleFavorite(pattern.Name)}
>
{#if $favorites.includes(pattern.Name)}
<span class="text-yellow-400"></span>
{:else}
<span class="text-primary-400 hover:text-yellow-300"></span>
{/if}
</button>
</div>
<p class="text-sm text-muted-foreground break-words leading-relaxed">{pattern.Description}</p>
</div>
{/each}
</div>
{/if}
</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>
patterns={$patterns}
on:tagsChanged={handleTagFilter}
bind:this={tagFilterRef}
hideToggleButton={false}
/>
</div>
<style>
.patterns-container {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
-ms-overflow-style: thin;
}
/* Custom scrollbar styling */
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.patterns-list {
display: flex;
flex-direction: column;
width: 100%;
max-width: 560px;
margin: 0 auto;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.2);
border-radius: 4px;
}
.pattern-item {
display: flex;
flex-direction: column;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(156, 163, 175, 0.3);
border-radius: 4px;
}
.pattern-item:last-child {
border-bottom: none;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.5);
}
/* h3.pattern-name {
word-break: break-all;
hyphens: auto;
overflow-wrap: break-word;
} */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.3) rgba(31, 41, 55, 0.2);
}
.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

@@ -7,17 +7,10 @@
}>();
export let patterns: Pattern[];
export let hideToggleButton = false; // New prop to hide the toggle button when used in modal
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)
@@ -36,15 +29,16 @@
}
</script>
<div class="tag-panel {isExpanded ? 'expanded' : ''}" style="z-index: 50">
<div class="tag-panel {isExpanded ? 'expanded' : ''} {hideToggleButton ? 'embedded' : ''}" style="z-index: 50">
{#if !hideToggleButton}
<div class="panel-header">
<button class="close-btn" on:click={togglePanel}>
{isExpanded ? 'Close Filter Tags ◀' : 'Open Filter Tags ▶'}
</button>
</div>
{/if}
<div class="panel-content">
<div class="panel-content {hideToggleButton ? 'always-visible' : ''}">
<div class="reset-container">
<button
class="reset-btn"
@@ -56,7 +50,7 @@
Reset All Tags
</button>
</div>
{#each Array.from(new Set(patterns.flatMap(p => p.tags))).sort() as tag}
{#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)}
@@ -67,6 +61,7 @@
</div>
</div>
<style>
/* Default positioning for standalone mode */
.tag-panel {
position: fixed; /* Change to fixed positioning */
left: calc(50% + 300px); /* Position starts after modal's right edge */
@@ -76,19 +71,37 @@
transition: left 0.3s ease;
}
/* When embedded in another component, use relative positioning */
.tag-panel.embedded {
position: relative;
left: auto;
top: auto;
transform: none;
width: 100%;
height: 100%;
}
.tag-panel.expanded {
left: calc(50% + 360px); /* Final position just to the right of modal */
}
.panel-content {
.panel-content {
display: none;
padding: 12px;
flex-wrap: wrap;
gap: 6px;
max-height: 80vh;
overflow-y: auto;
grid-template-columns: repeat(3, 1fr);
}
/* Adjust max-height when embedded */
.embedded .panel-content {
max-height: 100%;
}
/* When used in modal, always show content */
.panel-content.always-visible {
display: flex;
}
.tag-brick {
@@ -102,7 +115,6 @@
overflow: hidden;
}
.reset-container {
width: 100%;
padding-bottom: 8px;
@@ -124,29 +136,9 @@
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;
.expanded .panel-content {
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;
@@ -178,17 +170,11 @@
margin-left: -50px;
}
.close-btn:hover {
background: rgba(255,255,255,0.1);
}
.tag-brick.selected {
background: var(--primary-300);
}
.tag-brick.selected {
background: var(--primary-300);
}
</style>

View File

@@ -9,15 +9,23 @@
export let show = false;
</script>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions a11y-no-static-element-interactions -->
{#if show}
<div
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 mt-2"
on:click={() => dispatch('close')}
on:keydown={(e) => e.key === 'Escape' && dispatch('close')}
role="dialog"
aria-modal="true"
aria-label="Modal dialog"
tabindex="-1"
transition:fade={{ duration: 200 }}
>
<div
class="relative"
on:click|stopPropagation
role="document"
aria-label="Modal content"
transition:scale={{ duration: 200 }}
>
<slot />

View File

@@ -0,0 +1,333 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import TagFilterPanel from '$lib/components/patterns/TagFilterPanel.svelte';
let tagFilterRef: TagFilterPanel;
let selectedTags: string[] = [];
import { cn } from "$lib/utils/utils";
import type { Pattern } from '$lib/interfaces/pattern-interface';
import { patterns, patternAPI, selectedPatternName } from '$lib/store/pattern-store';
import { favorites } from '$lib/store/favorites-store';
import { Input } from "$lib/components/ui/input";
const dispatch = createEventDispatcher();
let isTagPanelOpen = false;
let searchQuery = '';
let showOnlyFavorites = false;
onMount(async () => {
try {
await patternAPI.loadPatterns();
} catch (error) {
console.error('Error loading patterns:', error);
}
});
function toggleFavorite(patternName: string) {
favorites.toggleFavorite(patternName);
}
function selectPattern(patternName: string) {
patternAPI.selectPattern(patternName);
dispatch('select', patternName);
}
function closeModal() {
dispatch('close');
}
function toggleTagPanel() {
isTagPanelOpen = !isTagPanelOpen;
}
function handleTagFilter(event: CustomEvent<string[]>) {
selectedTags = event.detail;
}
function toggleFavoritesFilter() {
showOnlyFavorites = !showOnlyFavorites;
}
// Apply filtering based on search query, favorites filter, and tag selection
$: filteredPatterns = $patterns
.filter(p => {
// Apply favorites filter if enabled
if (showOnlyFavorites && !$favorites.includes(p.Name)) {
return false;
}
// Apply tag filter if any tags are selected
if (selectedTags.length > 0) {
if (!p.tags || !selectedTags.every(tag => p.tags.includes(tag))) {
return false;
}
}
// Apply search filter if query exists
if (searchQuery.trim()) {
return (
p.Name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.Description.toLowerCase().includes(searchQuery.toLowerCase()) ||
(p.tags && p.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
);
}
return true;
});
</script>
<!-- Main container with flexible layout -->
<div class="flex h-[85vh]">
<!-- Modal container with responsive positioning -->
<div class={cn(
"flex flex-col bg-primary-800 rounded-lg shadow-xl transition-all duration-300",
isTagPanelOpen
? "w-[75vw]"
: "w-full max-w-[95vw] mx-auto"
)}>
<!-- Header with grid layout -->
<div class="grid grid-cols-[auto_auto_1fr_auto] items-center p-4 border-b border-primary-700/30 sticky top-0 bg-primary-800 z-10">
<!-- Left column: Title -->
<h2 class="text-xl font-semibold text-primary-200 mr-4">Pattern Library</h2>
<!-- Second column: Search -->
<div class="mr-4">
<Input
bind:value={searchQuery}
placeholder="Search patterns..."
class="w-full min-w-[300px] text-emerald-900"
/>
</div>
<!-- Third column: Favorites button -->
<div class="flex items-center">
<button
on:click={toggleFavoritesFilter}
class={cn(
"px-3 py-1.5 rounded-md text-sm font-medium transition-all",
showOnlyFavorites
? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30"
: "bg-primary-700/30 text-primary-300 border border-primary-600/20 hover:bg-primary-700/50"
)}
>
<span class="mr-1">{showOnlyFavorites ? "★" : "☆"}</span>
Favorites
</button>
</div>
<!-- Fourth column: Other controls -->
<div class="flex items-center gap-3 justify-end">
<!-- Single tag panel toggle button -->
<button
on:click={toggleTagPanel}
class={cn(
"px-3 py-1.5 rounded-md text-sm font-medium transition-all",
isTagPanelOpen
? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
: "bg-primary-700/30 text-primary-300 border border-primary-600/20 hover:bg-primary-700/50"
)}
>
{isTagPanelOpen ? "Close Filter Tags ◀" : "Open Filter Tags ▶"}
</button>
<!-- Close modal button -->
<button
on:click={closeModal}
class="px-2 py-2 rounded-full bg-primary-700/40 text-primary-200 hover:bg-primary-700/60 hover:text-primary-100"
aria-label="Close modal"
>
<span class="text-xl font-bold">×</span>
</button>
</div>
</div>
<!-- Selected tags display -->
{#if selectedTags.length > 0}
<div class="px-4 pb-2 pt-2 border-b border-primary-700/30">
<div class="text-sm text-white/70 bg-primary-700/30 rounded-md p-2 flex justify-between items-center">
<div class="flex flex-wrap gap-1 items-center">
<span class="mr-1">Tags:</span>
{#each selectedTags as tag}
<div class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary-600/40 text-primary-200 border border-primary-500/30">
{tag}
<button
class="ml-1 text-xs text-primary-300 hover:text-primary-100"
on:click={() => {
selectedTags = selectedTags.filter(t => t !== tag);
}}
>
×
</button>
</div>
{/each}
</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 = [];
if (tagFilterRef && typeof tagFilterRef.reset === 'function') {
tagFilterRef.reset();
}
}}
>
reset
</button>
</div>
</div>
{/if}
<!-- Pattern tiles grid with scrolling -->
<div class="flex-1 overflow-y-auto p-4 pattern-grid-container">
{#if filteredPatterns.length === 0}
<div class="flex justify-center items-center h-full">
<p class="text-primary-300">
{showOnlyFavorites
? "No favorite patterns found. Add some favorites first!"
: "No patterns found matching your search."}
</p>
</div>
{:else}
<div class={cn(
"grid grid-cols-1 sm:grid-cols-2 gap-4",
isTagPanelOpen
? "md:grid-cols-2 lg:grid-cols-3"
: "md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
)}>
{#each filteredPatterns as pattern}
<button
class="text-left border-2 border-primary-600/40 rounded-lg shadow-md hover:shadow-lg p-4 flex flex-col h-58 bg-primary-700/30 hover:bg-primary-700/50 transition-all transform hover:-translate-y-1 duration-200"
on:click={() => selectPattern(pattern.Name)}
>
<div class="flex justify-between items-start mb-2">
<h3 class="pattern-name font-bold text-base text-primary-200 leading-tight break-all overflow-hidden pr-2 w-[85%]">{pattern.Name}</h3>
<button
on:click|stopPropagation={() => toggleFavorite(pattern.Name)}
class="focus:outline-none ml-1 mt-0.5"
aria-label={$favorites.includes(pattern.Name) ? 'Remove from favorites' : 'Add to favorites'}
>
{#if $favorites.includes(pattern.Name)}
<span class="text-yellow-400 text-xl"></span>
{:else}
<span class="text-primary-400 text-xl hover:text-yellow-300"></span>
{/if}
</button>
</div>
<!-- Pattern description with scrolling if needed -->
<div class="flex-grow overflow-y-auto mb-1 pr-1 custom-scrollbar">
<p class="text-sm text-primary-300/90 leading-relaxed">{pattern.Description}</p>
</div>
<!-- Tags section -->
{#if pattern.tags && pattern.tags.length > 0}
<div class="flex flex-wrap gap-1 mt-2">
{#each pattern.tags as tag}
<span class="inline-flex items-center px-1 py-0.25 rounded-full text-[8px] font-medium bg-primary-600/40 text-primary-200 border border-primary-500/30">
{tag}
</span>
{/each}
</div>
{/if}
</button>
{/each}
</div>
{/if}
</div>
</div>
<!-- Tag filter panel - positioned on the right when open -->
{#if isTagPanelOpen}
<div class="tag-panel-container">
<div class="tag-panel-header">
<button class="tag-panel-close" on:click={toggleTagPanel}>
<span class="text-lg">×</span>
</button>
<h3 class="text-sm font-medium text-primary-200">Filter Tags</h3>
</div>
<div class="tag-panel-content">
<TagFilterPanel
patterns={$patterns}
on:tagsChanged={handleTagFilter}
bind:this={tagFilterRef}
hideToggleButton={true}
/>
</div>
</div>
{/if}
</div>
<style>
/* Custom scrollbar styling remains the same */
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.2);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(156, 163, 175, 0.3);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.5);
}
/* Add this to your <style> section */
h3.pattern-name {
word-break: break-all; /* Force breaks anywhere if needed */
hyphens: auto; /* Enable hyphenation */
overflow-wrap: break-word; /* Fallback */
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.3) rgba(31, 41, 55, 0.2);
}
.pattern-grid-container {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.3) rgba(31, 41, 55, 0.2);
}
/* Tag panel styling */
.tag-panel-container {
width: 20vw;
height: 100%;
background-color: #1e293b; /* Use a solid color instead of var */
border-left: 1px solid rgba(255, 255, 255, 0.1);
z-index: 20;
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
}
.tag-panel-header {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.tag-panel-close {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
margin-right: 8px;
cursor: pointer;
}
.tag-panel-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.tag-panel-content {
height: calc(100% - 49px);
overflow-y: auto;
}
</style>

View File

@@ -14,6 +14,7 @@
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions a11y-mouse-events-have-key-events -->
<div class="tooltip-container">
<div
class="tooltip-trigger"
@@ -21,6 +22,8 @@
on:mouseleave={hideTooltip}
on:focusin={showTooltip}
on:focusout={hideTooltip}
role="tooltip"
aria-label="Tooltip trigger"
>
<slot />
</div>
@@ -33,9 +36,11 @@
class:bottom="{position === 'bottom'}"
class:left="{position === 'left'}"
class:right="{position === 'right'}"
role="tooltip"
aria-label={text}
>
{text}
<div class="tooltip-arrow" />
<div class="tooltip-arrow" role="presentation" />
</div>
{/if}
</div>

File diff suppressed because it is too large Load Diff