mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-09 22:38:10 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2579d37c16 | ||
|
|
4f28d85e96 | ||
|
|
f529b8bb80 | ||
|
|
71437605e1 | ||
|
|
cf5753a186 | ||
|
|
433c83fe2c | ||
|
|
01770cc6e3 | ||
|
|
55fda5e025 | ||
|
|
daad5f986e | ||
|
|
3785d0a5fa | ||
|
|
8a326e9cfb | ||
|
|
5f5822f1c6 | ||
|
|
111482e46e | ||
|
|
9b56e0e996 | ||
|
|
9b830f9801 | ||
|
|
dda73d3333 | ||
|
|
40c26d9c9e | ||
|
|
cdd86b0ed9 | ||
|
|
c2e84d6db9 | ||
|
|
4208a02191 | ||
|
|
943b26eeef | ||
|
|
d6ceae9efd | ||
|
|
f57dc6d681 | ||
|
|
4e4bfc9d5d | ||
|
|
ea137c1525 | ||
|
|
f0be1d4735 | ||
|
|
e3975b9364 | ||
|
|
cd48802ea0 | ||
|
|
dceccd8e72 | ||
|
|
0813ad9c39 | ||
|
|
e391132167 | ||
|
|
c7f86d3a0c | ||
|
|
f0d92f9424 | ||
|
|
4a9bdb1479 | ||
|
|
1fe8afd329 | ||
|
|
e89ccf5e97 | ||
|
|
a227e61952 |
108
README.md
108
README.md
@@ -15,7 +15,7 @@
|
||||
</p>
|
||||
|
||||
[Updates](#updates) •
|
||||
[What and Why](#whatandwhy) •
|
||||
[What and Why](#what-and-why) •
|
||||
[Philosophy](#philosophy) •
|
||||
[Installation](#Installation) •
|
||||
[Usage](#Usage) •
|
||||
@@ -41,11 +41,14 @@
|
||||
- [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)
|
||||
- [Windows](#windows)
|
||||
- [macOS (arm64)](#macos-arm64)
|
||||
- [macOS (amd64)](#macos-amd64)
|
||||
- [Linux (amd64)](#linux-amd64)
|
||||
- [Linux (arm64)](#linux-arm64)
|
||||
- [Using package managers](#using-package-managers)
|
||||
- [macOS (Homebrew)](#macos-homebrew)
|
||||
- [Arch Linux (AUR)](#arch-linux-aur)
|
||||
- [From Source](#from-source)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Setup](#setup)
|
||||
@@ -62,10 +65,12 @@
|
||||
- [Helper Apps](#helper-apps)
|
||||
- [`to_pdf`](#to_pdf)
|
||||
- [`to_pdf` Installation](#to_pdf-installation)
|
||||
- [`code_helper`](#code_helper)
|
||||
- [pbpaste](#pbpaste)
|
||||
- [Web Interface](#web-interface)
|
||||
- [Installing](#installing)
|
||||
- [Streamlit UI](#streamlit-ui)
|
||||
- [Clipboard Support](#clipboard-support)
|
||||
- [Meta](#meta)
|
||||
- [Primary contributors](#primary-contributors)
|
||||
|
||||
@@ -90,7 +95,7 @@ Fabric was created to address this by enabling everyone to granularly apply AI t
|
||||
|
||||
## Intro videos
|
||||
|
||||
Keep in mind that many of these were recorded when Fabric was Python-based, so remember to use the current [install instructions](#Installation) below.
|
||||
Keep in mind that many of these were recorded when Fabric was Python-based, so remember to use the current [install instructions](#installation) below.
|
||||
|
||||
- [Network Chuck](https://www.youtube.com/watch?v=UbDyjIIGaxQ)
|
||||
- [David Bombal](https://www.youtube.com/watch?v=vF-MQmVxnCs)
|
||||
@@ -113,7 +118,7 @@ Our approach is to break problems into individual pieces (see below) and then ap
|
||||
|
||||
Prompts are good for this, but the biggest challenge I faced in 2023——which still exists today—is **the sheer number of AI prompts out there**. We all have prompts that are useful, but it's hard to discover new ones, know if they are good or not, _and manage different versions of the ones we like_.
|
||||
|
||||
One of <code>fabric</code>'s primary features is helping people collect and integrate prompts, which we call _Patterns_, into various parts of their lives.
|
||||
One of `fabric`'s primary features is helping people collect and integrate prompts, which we call _Patterns_, into various parts of their lives.
|
||||
|
||||
Fabric has Patterns for all sorts of life and work activities, including:
|
||||
|
||||
@@ -134,21 +139,43 @@ To install Fabric, you can use the latest release binaries or install it from th
|
||||
|
||||
### Get Latest Release Binaries
|
||||
|
||||
#### Windows:
|
||||
#### Windows
|
||||
|
||||
`https://github.com/danielmiessler/fabric/releases/latest/download/fabric-windows-amd64.exe`
|
||||
|
||||
#### MacOS (arm64):
|
||||
#### macOS (arm64)
|
||||
|
||||
`curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-darwin-arm64 > fabric && chmod +x fabric && ./fabric --version`
|
||||
|
||||
#### MacOS (amd64):
|
||||
#### macOS (amd64)
|
||||
|
||||
`curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-darwin-amd64 > fabric && chmod +x fabric && ./fabric --version`
|
||||
|
||||
#### Linux (amd64):
|
||||
#### Linux (amd64)
|
||||
|
||||
`curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-linux-amd64 > fabric && chmod +x fabric && ./fabric --version`
|
||||
|
||||
#### Linux (arm64):
|
||||
#### Linux (arm64)
|
||||
|
||||
`curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-linux-arm64 > fabric && chmod +x fabric && ./fabric --version`
|
||||
|
||||
### Using package managers
|
||||
|
||||
**NOTE:** using Homebrew or the Arch Linux package managers makes `fabric` available as `fabric-ai`, so add
|
||||
the following alias to your shell startup files to account for this:
|
||||
|
||||
```bash
|
||||
alias fabric='fabric-ai'
|
||||
```
|
||||
|
||||
#### macOS (Homebrew)
|
||||
|
||||
`brew install fabric-ai`
|
||||
|
||||
#### Arch Linux (AUR)
|
||||
|
||||
`yay -S fabric-ai`
|
||||
|
||||
### From Source
|
||||
|
||||
To install Fabric, [make sure Go is installed](https://go.dev/doc/install), and then run the following command.
|
||||
@@ -317,7 +344,7 @@ This also creates a `yt` alias that allows you to use `yt https://www.youtube.co
|
||||
|
||||
#### Save your files in markdown using aliases
|
||||
|
||||
If in addition to the above aliases you would like to have the option to save the output to your favourite markdown note vault like Obsidian then instead of the above add the following to your `.zshrc` or `.bashrc` file:
|
||||
If in addition to the above aliases you would like to have the option to save the output to your favorite markdown note vault like Obsidian then instead of the above add the following to your `.zshrc` or `.bashrc` file:
|
||||
|
||||
```bash
|
||||
# Define the base directory for Obsidian notes
|
||||
@@ -328,7 +355,7 @@ for pattern_file in ~/.config/fabric/patterns/*; do
|
||||
# Get the base name of the file (i.e., remove the directory path)
|
||||
pattern_name=$(basename "$pattern_file")
|
||||
|
||||
# Unalias any existing alias with the same name
|
||||
# Remove any existing alias with the same name
|
||||
unalias "$pattern_name" 2>/dev/null
|
||||
|
||||
# Define a function dynamically for each pattern
|
||||
@@ -370,7 +397,7 @@ go install github.com/danielmiessler/fabric@latest
|
||||
fabric --setup
|
||||
```
|
||||
|
||||
Then [set your environmental variables](#environmental-variables) as shown above.
|
||||
Then [set your environmental variables](#environment-variables) as shown above.
|
||||
|
||||
### Upgrading
|
||||
|
||||
@@ -477,31 +504,29 @@ Now let's look at some things you can do with Fabric.
|
||||
|
||||
1. Run the `summarize` Pattern based on input from `stdin`. In this case, the body of an article.
|
||||
|
||||
```bash
|
||||
pbpaste | fabric --pattern summarize
|
||||
```
|
||||
```bash
|
||||
pbpaste | fabric --pattern summarize
|
||||
```
|
||||
|
||||
2. Run the `analyze_claims` Pattern with the `--stream` option to get immediate and streaming results.
|
||||
|
||||
```bash
|
||||
pbpaste | fabric --stream --pattern analyze_claims
|
||||
```
|
||||
```bash
|
||||
pbpaste | fabric --stream --pattern analyze_claims
|
||||
```
|
||||
|
||||
3. Run the `extract_wisdom` Pattern with the `--stream` option to get immediate and streaming results from any Youtube video (much like in the original introduction video).
|
||||
|
||||
```bash
|
||||
fabric -y "https://youtube.com/watch?v=uXs-zPc63kM" --stream --pattern extract_wisdom
|
||||
```
|
||||
3. Run the `extract_wisdom` Pattern with the `--stream` option to get immediate and streaming results from any Youtube video (much like in the original introduction video).
|
||||
|
||||
```bash
|
||||
fabric -y "https://youtube.com/watch?v=uXs-zPc63kM" --stream --pattern extract_wisdom
|
||||
```
|
||||
|
||||
4. Create patterns- you must create a .md file with the pattern and save it to `~/.config/fabric/patterns/[yourpatternname]`.
|
||||
|
||||
|
||||
5. Run a `analyze_claims` pattern on a website. Fabric uses Jina AI to scrape the URL into markdown format before sending it to the model.
|
||||
|
||||
```bash
|
||||
fabric -u https://github.com/danielmiessler/fabric/ -p analyze_claims
|
||||
```
|
||||
```bash
|
||||
fabric -u https://github.com/danielmiessler/fabric/ -p analyze_claims
|
||||
```
|
||||
|
||||
## Just use the Patterns
|
||||
|
||||
@@ -539,15 +564,10 @@ You may want to use Fabric to create your own custom Patterns—but not share th
|
||||
|
||||
Just make a directory in `~/.config/custompatterns/` (or wherever) and put your `.md` files in there.
|
||||
|
||||
When you're ready to use them, copy them into:
|
||||
|
||||
```
|
||||
~/.config/fabric/patterns/
|
||||
```
|
||||
When you're ready to use them, copy them into `~/.config/fabric/patterns/`
|
||||
|
||||
You can then use them like any other Patterns, but they won't be public unless you explicitly submit them as Pull Requests to the Fabric project. So don't worry—they're private to you.
|
||||
|
||||
|
||||
## Helper Apps
|
||||
|
||||
Fabric also makes use of some core helper apps (tools) to make it easier to integrate with your various workflows. Here are some examples:
|
||||
@@ -580,6 +600,20 @@ go install github.com/danielmiessler/fabric/plugins/tools/to_pdf@latest
|
||||
|
||||
Make sure you have a LaTeX distribution (like TeX Live or MiKTeX) installed on your system, as `to_pdf` requires `pdflatex` to be available in your system's PATH.
|
||||
|
||||
### `code_helper`
|
||||
|
||||
`code_helper` is used in conjunction with the `create_coding_feature` pattern.
|
||||
It generates a `json` representation of a directory of code that can be fed into an AI model
|
||||
with instructions to create a new feature or edit the code in a specified way.
|
||||
|
||||
See [the Create Coding Feature Pattern README](./patterns/create_coding_feature/README.md) for details.
|
||||
|
||||
Install it first using:
|
||||
|
||||
```bash
|
||||
go install github.com/danielmiessler/fabric/plugins/tools/code_helper@latest
|
||||
```
|
||||
|
||||
## pbpaste
|
||||
|
||||
The [examples](#examples) use the macOS program `pbpaste` to paste content from the clipboard to pipe into `fabric` as the input. `pbpaste` is not available on Windows or Linux, but there are alternatives.
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -29,7 +29,7 @@ func (a *Attachment) GetId() (ret string, err error) {
|
||||
hash = fmt.Sprintf("%x", sha256.Sum256(a.Content))
|
||||
} else if a.Path != nil {
|
||||
var content []byte
|
||||
if content, err = ioutil.ReadFile(*a.Path); err != nil {
|
||||
if content, err = os.ReadFile(*a.Path); err != nil {
|
||||
return
|
||||
}
|
||||
hash = fmt.Sprintf("%x", sha256.Sum256(content))
|
||||
@@ -83,7 +83,7 @@ func (a *Attachment) ContentBytes() (ret []byte, err error) {
|
||||
return
|
||||
}
|
||||
if a.Path != nil {
|
||||
if ret, err = ioutil.ReadFile(*a.Path); err != nil {
|
||||
if ret, err = os.ReadFile(*a.Path); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -94,7 +94,7 @@ func (a *Attachment) ContentBytes() (ret []byte, err error) {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if ret, err = ioutil.ReadAll(resp.Body); err != nil {
|
||||
if ret, err = io.ReadAll(resp.Body); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
195
common/file_manager.go
Normal file
195
common/file_manager.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileChangesMarker identifies the start of a file changes section in output
|
||||
const FileChangesMarker = "__CREATE_CODING_FEATURE_FILE_CHANGES__"
|
||||
|
||||
const (
|
||||
// MaxFileSize is the maximum size of a file that can be created (10MB)
|
||||
MaxFileSize = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
// FileChange represents a single file change operation to be performed
|
||||
type FileChange struct {
|
||||
Operation string `json:"operation"` // "create" or "update"
|
||||
Path string `json:"path"` // Relative path from project root
|
||||
Content string `json:"content"` // New file content
|
||||
}
|
||||
|
||||
// ParseFileChanges extracts and parses the file change marker section from LLM output
|
||||
func ParseFileChanges(output string) (changeSummary string, changes []FileChange, err error) {
|
||||
fileChangesStart := strings.Index(output, FileChangesMarker)
|
||||
if fileChangesStart == -1 {
|
||||
return output, nil, nil // No file changes section found
|
||||
}
|
||||
changeSummary = output[:fileChangesStart] // Everything before the marker
|
||||
|
||||
// Extract the JSON part
|
||||
jsonStart := fileChangesStart + len(FileChangesMarker)
|
||||
// Find the first [ after the file changes marker
|
||||
jsonArrayStart := strings.Index(output[jsonStart:], "[")
|
||||
if jsonArrayStart == -1 {
|
||||
return output, nil, fmt.Errorf("invalid %s format: no JSON array found", FileChangesMarker)
|
||||
}
|
||||
jsonStart += jsonArrayStart
|
||||
|
||||
// Find the matching closing bracket for the array with proper bracket counting
|
||||
bracketCount := 0
|
||||
jsonEnd := jsonStart
|
||||
for i := jsonStart; i < len(output); i++ {
|
||||
if output[i] == '[' {
|
||||
bracketCount++
|
||||
} else if output[i] == ']' {
|
||||
bracketCount--
|
||||
if bracketCount == 0 {
|
||||
jsonEnd = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bracketCount != 0 {
|
||||
return output, nil, fmt.Errorf("invalid %s format: unbalanced brackets", FileChangesMarker)
|
||||
}
|
||||
|
||||
// Extract the JSON string and fix escape sequences
|
||||
jsonStr := output[jsonStart:jsonEnd]
|
||||
|
||||
// Fix specific invalid escape sequences
|
||||
// First try with the common \C issue
|
||||
jsonStr = strings.Replace(jsonStr, `\C`, `\\C`, -1)
|
||||
|
||||
// Parse the JSON
|
||||
var fileChanges []FileChange
|
||||
err = json.Unmarshal([]byte(jsonStr), &fileChanges)
|
||||
if err != nil {
|
||||
// If still failing, try a more comprehensive fix
|
||||
jsonStr = fixInvalidEscapes(jsonStr)
|
||||
err = json.Unmarshal([]byte(jsonStr), &fileChanges)
|
||||
if err != nil {
|
||||
return changeSummary, nil, fmt.Errorf("failed to parse %s JSON: %w", FileChangesMarker, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate file changes
|
||||
for i, change := range fileChanges {
|
||||
// Validate operation
|
||||
if change.Operation != "create" && change.Operation != "update" {
|
||||
return changeSummary, nil, fmt.Errorf("invalid operation for file change %d: %s", i, change.Operation)
|
||||
}
|
||||
|
||||
// Validate path
|
||||
if change.Path == "" {
|
||||
return changeSummary, nil, fmt.Errorf("empty path for file change %d", i)
|
||||
}
|
||||
|
||||
// Check for suspicious paths (directory traversal)
|
||||
if strings.Contains(change.Path, "..") {
|
||||
return changeSummary, nil, fmt.Errorf("suspicious path for file change %d: %s", i, change.Path)
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if len(change.Content) > MaxFileSize {
|
||||
return changeSummary, nil, fmt.Errorf("file content too large for file change %d: %d bytes", i, len(change.Content))
|
||||
}
|
||||
}
|
||||
|
||||
return changeSummary, fileChanges, nil
|
||||
}
|
||||
|
||||
// fixInvalidEscapes replaces invalid escape sequences in JSON strings
|
||||
func fixInvalidEscapes(jsonStr string) string {
|
||||
validEscapes := []byte{'b', 'f', 'n', 'r', 't', '\\', '/', '"', 'u'}
|
||||
|
||||
var result strings.Builder
|
||||
inQuotes := false
|
||||
i := 0
|
||||
|
||||
for i < len(jsonStr) {
|
||||
ch := jsonStr[i]
|
||||
|
||||
// Track whether we're inside a JSON string
|
||||
if ch == '"' && (i == 0 || jsonStr[i-1] != '\\') {
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
|
||||
// Handle actual control characters inside string literals
|
||||
if inQuotes {
|
||||
// Convert literal control characters to proper JSON escape sequences
|
||||
if ch == '\n' {
|
||||
result.WriteString("\\n")
|
||||
i++
|
||||
continue
|
||||
} else if ch == '\r' {
|
||||
result.WriteString("\\r")
|
||||
i++
|
||||
continue
|
||||
} else if ch == '\t' {
|
||||
result.WriteString("\\t")
|
||||
i++
|
||||
continue
|
||||
} else if ch < 32 {
|
||||
// Handle other control characters
|
||||
fmt.Fprintf(&result, "\\u%04x", ch)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check for escape sequences only inside strings
|
||||
if inQuotes && ch == '\\' && i+1 < len(jsonStr) {
|
||||
nextChar := jsonStr[i+1]
|
||||
isValid := false
|
||||
|
||||
for _, validEscape := range validEscapes {
|
||||
if nextChar == validEscape {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
// Invalid escape sequence - add an extra backslash
|
||||
result.WriteByte('\\')
|
||||
result.WriteByte('\\')
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result.WriteByte(ch)
|
||||
i++
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// ApplyFileChanges applies the parsed file changes to the file system
|
||||
func ApplyFileChanges(projectRoot string, changes []FileChange) error {
|
||||
for i, change := range changes {
|
||||
// Get the absolute path
|
||||
absPath := filepath.Join(projectRoot, change.Path)
|
||||
|
||||
// Create directories if necessary
|
||||
dir := filepath.Dir(absPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s for file change %d: %w", dir, i, err)
|
||||
}
|
||||
|
||||
// Write the file
|
||||
if err := os.WriteFile(absPath, []byte(change.Content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write file %s for file change %d: %w", absPath, i, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Applied %s operation to %s\n", change.Operation, change.Path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
185
common/file_manager_test.go
Normal file
185
common/file_manager_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseFileChanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want int // number of expected file changes
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "No " + FileChangesMarker + " section",
|
||||
input: "This is a normal response with no file changes.",
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid " + FileChangesMarker + " section",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "test.txt",
|
||||
"content": "Hello, World!"
|
||||
},
|
||||
{
|
||||
"operation": "update",
|
||||
"path": "other.txt",
|
||||
"content": "Updated content"
|
||||
}
|
||||
]
|
||||
Some text after.`,
|
||||
want: 2,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON in " + FileChangesMarker + " section",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "test.txt",
|
||||
"content": "Hello, World!"
|
||||
},
|
||||
{
|
||||
"operation": "invalid",
|
||||
"path": "other.txt"
|
||||
"content": "Updated content"
|
||||
}
|
||||
]`,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid operation",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "delete",
|
||||
"path": "test.txt",
|
||||
"content": ""
|
||||
}
|
||||
]`,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Empty path",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "",
|
||||
"content": "Hello, World!"
|
||||
}
|
||||
]`,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Suspicious path with directory traversal",
|
||||
input: `Some text before.
|
||||
` + FileChangesMarker + `
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "../etc/passwd",
|
||||
"content": "Hello, World!"
|
||||
}
|
||||
]`,
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, got, err := ParseFileChanges(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseFileChanges() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && len(got) != tt.want {
|
||||
t.Errorf("ParseFileChanges() got %d file changes, want %d", len(got), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyFileChanges(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "file-manager-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
// Test file changes
|
||||
changes := []FileChange{
|
||||
{
|
||||
Operation: "create",
|
||||
Path: "test.txt",
|
||||
Content: "Hello, World!",
|
||||
},
|
||||
{
|
||||
Operation: "create",
|
||||
Path: "subdir/nested.txt",
|
||||
Content: "Nested content",
|
||||
},
|
||||
}
|
||||
|
||||
// Apply the changes
|
||||
if err := ApplyFileChanges(tempDir, changes); err != nil {
|
||||
t.Fatalf("ApplyFileChanges() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify the first file was created correctly
|
||||
content, err := os.ReadFile(filepath.Join(tempDir, "test.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read created file: %v", err)
|
||||
}
|
||||
if string(content) != "Hello, World!" {
|
||||
t.Errorf("File content = %q, want %q", string(content), "Hello, World!")
|
||||
}
|
||||
|
||||
// Verify the nested file was created correctly
|
||||
content, err = os.ReadFile(filepath.Join(tempDir, "subdir/nested.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read created nested file: %v", err)
|
||||
}
|
||||
if string(content) != "Nested content" {
|
||||
t.Errorf("Nested file content = %q, want %q", string(content), "Nested content")
|
||||
}
|
||||
|
||||
// Test updating a file
|
||||
updateChanges := []FileChange{
|
||||
{
|
||||
Operation: "update",
|
||||
Path: "test.txt",
|
||||
Content: "Updated content",
|
||||
},
|
||||
}
|
||||
|
||||
// Apply the update
|
||||
if err := ApplyFileChanges(tempDir, updateChanges); err != nil {
|
||||
t.Fatalf("ApplyFileChanges() error = %v", err)
|
||||
}
|
||||
// Verify the file was updated correctly
|
||||
content, err = os.ReadFile(filepath.Join(tempDir, "test.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated file: %v", err)
|
||||
}
|
||||
if string(content) != "Updated content" {
|
||||
t.Errorf("Updated file content = %q, want %q", string(content), "Updated content")
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
goopenai "github.com/sashabaranov/go-openai"
|
||||
@@ -28,6 +30,7 @@ type Chatter struct {
|
||||
strategy string
|
||||
}
|
||||
|
||||
// Send processes a chat request and applies any file changes if using the create_coding_feature pattern
|
||||
func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (session *fsdb.Session, err error) {
|
||||
if session, err = o.BuildSession(request, opts.Raw); err != nil {
|
||||
return
|
||||
@@ -79,6 +82,30 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
|
||||
return
|
||||
}
|
||||
|
||||
// Process file changes if using the create_coding_feature pattern
|
||||
if request.PatternName == "create_coding_feature" {
|
||||
// Look for file changes in the response
|
||||
summary, fileChanges, parseErr := common.ParseFileChanges(message)
|
||||
if parseErr != nil {
|
||||
fmt.Printf("Warning: Failed to parse file changes: %v\n", parseErr)
|
||||
} else if len(fileChanges) > 0 {
|
||||
// Get the project root - use the current directory
|
||||
projectRoot, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to get current directory: %v\n", err)
|
||||
// Continue without applying changes
|
||||
} else {
|
||||
if applyErr := common.ApplyFileChanges(projectRoot, fileChanges); applyErr != nil {
|
||||
fmt.Printf("Warning: Failed to apply file changes: %v\n", applyErr)
|
||||
} else {
|
||||
fmt.Println("Successfully applied file changes.")
|
||||
fmt.Printf("You can review the changes with 'git diff' if you're using git.\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
message = summary
|
||||
}
|
||||
|
||||
session.Append(&goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleAssistant, Content: message})
|
||||
|
||||
if session.Name != "" {
|
||||
@@ -185,7 +212,7 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
|
||||
|
||||
if session.IsEmpty() {
|
||||
session = nil
|
||||
err = fmt.Errorf(NoSessionPatternUserMessages)
|
||||
err = errors.New(NoSessionPatternUserMessages)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -19,7 +19,6 @@ require (
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/sashabaranov/go-openai v1.38.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/term v0.29.0
|
||||
golang.org/x/text v0.22.0
|
||||
google.golang.org/api v0.223.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
@@ -91,7 +90,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/arch v0.14.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/net v0.36.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -244,8 +244,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -242,8 +242,8 @@ schema = 3
|
||||
version = "v0.35.0"
|
||||
hash = "sha256-XT1VU0+m1nZbhrMYXN2+eaKBlScfiT4bCBgXu4mfa1Q="
|
||||
[mod."golang.org/x/net"]
|
||||
version = "v0.35.0"
|
||||
hash = "sha256-kCLhhvzHQCvUqC8kGhgMbVLUROG4ZeZNVGOVVv6tSAE="
|
||||
version = "v0.36.0"
|
||||
hash = "sha256-2c9AvnizBMCaqKPzwF2IMlRfvjRNDFmxIYhrgZjhOm0="
|
||||
[mod."golang.org/x/oauth2"]
|
||||
version = "v0.27.0"
|
||||
hash = "sha256-TBKV2c/m0SgPqrJSE0ltJXfImrYPafNuziLN25jgsYY="
|
||||
@@ -253,9 +253,6 @@ schema = 3
|
||||
[mod."golang.org/x/sys"]
|
||||
version = "v0.30.0"
|
||||
hash = "sha256-BuhWtwDkciVioc03rxty6G2vcZVnPX85lI7tgQOFVP8="
|
||||
[mod."golang.org/x/term"]
|
||||
version = "v0.29.0"
|
||||
hash = "sha256-aIupP/iNJKzHPUt0F7SaXc3u17h8plEPyQeypO7ilW8="
|
||||
[mod."golang.org/x/text"]
|
||||
version = "v0.22.0"
|
||||
hash = "sha256-kUwLNFk9K/YuWmO5/u2IshrmhT2CCuk+mAShSlTTeZo="
|
||||
|
||||
@@ -1 +1 @@
|
||||
"1.4.162"
|
||||
"1.4.166"
|
||||
|
||||
85
patterns/create_coding_feature/README.md
Normal file
85
patterns/create_coding_feature/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Create Coding Feature
|
||||
|
||||
Generate code changes to an existing coding project using AI.
|
||||
|
||||
## Installation
|
||||
|
||||
After installing the `code_helper` binary:
|
||||
|
||||
```bash
|
||||
go install github.com/danielmiessler/fabric/plugins/tools/code_helper@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The create_coding_feature allows you to apply AI-suggested code changes directly to your project files. Use it like this:
|
||||
|
||||
```bash
|
||||
code_helper [project_directory] "[instructions for code changes]" | fabric --pattern create_coding_feature
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
code_helper . "Create a simple Hello World C program in file main.c" | fabric --pattern create_coding_feature
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. `code_helper` scans your project directory and creates a JSON representation
|
||||
2. The AI model analyzes your project structure and instructions
|
||||
3. AI generates file changes in a standard format
|
||||
4. Fabric parses these changes and prompts you to confirm
|
||||
5. If confirmed, changes are applied to your project files
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```bash
|
||||
# Request AI to create a Hello World program
|
||||
code_helper . "Create a simple Hello World C program in file main.c" | fabric --pattern create_coding_feature
|
||||
|
||||
# Review the changes made to your project
|
||||
git diff
|
||||
|
||||
# Run/test the code
|
||||
make check
|
||||
|
||||
# If satisfied, commit the changes
|
||||
git add <changed files>
|
||||
git commit -s -m "Add Hello World program"
|
||||
```
|
||||
|
||||
### Security Enhancement Example
|
||||
|
||||
```bash
|
||||
code_helper . "Ensure that all user input is validated and sanitized before being used in the program." | fabric --pattern create_coding_feature
|
||||
git diff
|
||||
make check
|
||||
git add <changed files>
|
||||
git commit -s -m "Security fixes: Input validation"
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Always run from project root**: File changes are applied relative to your current directory
|
||||
- **Use with version control**: It's highly recommended to use this feature in a clean git repository so you can review and revert
|
||||
changes. You will *not* be asked to approve each change.
|
||||
|
||||
## Security Features
|
||||
|
||||
- Path validation to prevent directory traversal attempts
|
||||
- File size limits to prevent excessive file generation
|
||||
- Operation validation (only create/update operations allowed)
|
||||
- User confirmation required before applying changes
|
||||
|
||||
## Suggestions for Future Improvements
|
||||
|
||||
- Add a dry-run mode to show changes without applying them
|
||||
- Enhance reporting with detailed change summaries
|
||||
- Support for file deletions with safety checks
|
||||
- Add configuration options for project-specific rules
|
||||
- Provide rollback capability for applied changes
|
||||
- Add support for project-specific validation rules
|
||||
- Enhance script generation with conditional logic
|
||||
- Include detailed logging for API responses
|
||||
- Consider adding a GUI for ease of use
|
||||
117
patterns/create_coding_feature/system.md
Normal file
117
patterns/create_coding_feature/system.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You are an elite programmer. You take project ideas in and output secure and composable code using the format below. You always use the latest technology and best practices.
|
||||
|
||||
Take a deep breath and think step by step about how to best accomplish this goal using the following steps.
|
||||
|
||||
Input is a JSON file with the following format:
|
||||
|
||||
Example input:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "directory",
|
||||
"name": ".",
|
||||
"contents": [
|
||||
{
|
||||
"type": "file",
|
||||
"name": "README.md",
|
||||
"content": "This is the README.md file content"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "system.md",
|
||||
"content": "This is the system.md file contents"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "report",
|
||||
"directories": 1,
|
||||
"files": 5
|
||||
},
|
||||
{
|
||||
"type": "instructions",
|
||||
"name": "code_change_instructions",
|
||||
"details": "Update README and refactor main.py"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The object with `"type": "instructions"`, and field `"details"` contains the
|
||||
for the instructions for the suggested code changes. The `"name"` field is always
|
||||
`"code_change_instructions"`
|
||||
|
||||
The `"details"` field above, with type `"instructions"` contains the instructions for the suggested code changes.
|
||||
|
||||
## File Management Interface Instructions
|
||||
|
||||
You have access to a powerful file management system with the following capabilities:
|
||||
|
||||
### File Creation and Modification
|
||||
|
||||
- Use the **EXACT** JSON format below to define files that you want to be changed
|
||||
- If the file listed does not exist, it will be created
|
||||
- If a directory listed does not exist, it will be created
|
||||
- If the file already exists, it will be overwritten
|
||||
- It is **not possible** to delete files
|
||||
|
||||
```plaintext
|
||||
__CREATE_CODING_FEATURE_FILE_CHANGES__
|
||||
[
|
||||
{
|
||||
"operation": "create",
|
||||
"path": "README.md",
|
||||
"content": "This is the new README.md file content"
|
||||
},
|
||||
{
|
||||
"operation": "update",
|
||||
"path": "src/main.c",
|
||||
"content": "int main(){return 0;}"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Important Guidelines
|
||||
|
||||
- Always use relative paths from the project root
|
||||
- Provide complete, functional code when creating or modifying files
|
||||
- Be precise and concise in your file operations
|
||||
- Never create files outside of the project root
|
||||
|
||||
### Constraints
|
||||
|
||||
- Do not attempt to read or modify files outside the project root directory.
|
||||
- Ensure code follows best practices and is production-ready.
|
||||
- Handle potential errors gracefully in your code suggestions.
|
||||
- Do not trust external input to applications, assume users are malicious.
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Analyze the user's request
|
||||
2. Determine necessary file operations
|
||||
3. Provide clear, executable file creation/modification instructions
|
||||
4. Explain the purpose and functionality of proposed changes
|
||||
|
||||
## Output Sections
|
||||
|
||||
- Output a summary of the file changes
|
||||
- Output directory and file changes according to File Management Interface Instructions, in a json array marked by `__CREATE_CODING_FEATURE_FILE_CHANGES__`
|
||||
- Be exact in the `__CREATE_CODING_FEATURE_FILE_CHANGES__` section, and do not deviate from the proposed JSON format.
|
||||
- **never** omit the `__CREATE_CODING_FEATURE_FILE_CHANGES__` section.
|
||||
- If the proposed changes change how the project is built and installed, document these changes in the projects README.md
|
||||
- Implement build configurations changes if needed, prefer ninja if nothing already exists in the project, or is otherwise specified.
|
||||
- Document new dependencies according to best practices for the language used in the project.
|
||||
- Do not output sections that were not explicitly requested.
|
||||
|
||||
## Output Instructions
|
||||
|
||||
- Create the output using the formatting above
|
||||
- Do not output warnings or notes—just the requested sections.
|
||||
- Do not repeat items in the output sections
|
||||
- Be open to suggestions and output file system changes according to the JSON API described above
|
||||
- Output code that has comments for every step
|
||||
- Do not use deprecated features
|
||||
|
||||
## INPUT
|
||||
25
patterns/find_female_life_partner/system.md
Normal file
25
patterns/find_female_life_partner/system.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# IDENTITY AND PURPOSE
|
||||
|
||||
You are a relationship and marriage and life happiness expert AI with a 4,227 IQ. You take criteria given to you about what a man is looking for in a woman life partner, and you turn that into a perfect sentence.
|
||||
|
||||
# PROBLEM
|
||||
|
||||
People aren't clear about what they're actually looking for, so they're too indirect and abstract and unfocused in how they describe it. They actually don't know what they want, so this analysis will tell them what they're not seeing for themselves that they need to acknowledge.
|
||||
|
||||
# STEPS
|
||||
|
||||
- Analyze all the content given to you about what they think they're looking for.
|
||||
|
||||
- Figure out what they're skirting around and not saying directly.
|
||||
|
||||
- Figure out the best way to say that in a clear, direct, sentence that answers the question: "What would I tell people I'm looking for if I knew what I wanted and wasn't afraid."
|
||||
|
||||
- Write the perfect 24-word sentence in these versions:
|
||||
|
||||
1. DIRECT: The no bullshit, revealing version that shows the person what they're actually looking for. Only 8 words in extremely straightforward language.
|
||||
2. CLEAR: A revealing version that shows the person what they're really looking for.
|
||||
3. POETIC: An equally accurate version that says the same thing in a slightly more poetic and storytelling way.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Only output those two sentences, nothing else.
|
||||
@@ -215,7 +215,7 @@ Vacuous truth – a claim that is technically true but meaningless, in the form
|
||||
|
||||
- Don't use bold or italic formatting in the Markdown.
|
||||
|
||||
- Do no complain about the input data. Just do the task.
|
||||
- Do not complain about the input data. Just do the task.
|
||||
|
||||
# INPUT:
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
# Optional parameters:
|
||||
# @raycast.icon 🧠
|
||||
# @raycast.argument1 { "type": "text", "placeholder": "Input text", "optional": false, "percentEncoded": true}
|
||||
# @raycast.argument1 { "type": "text", "placeholder": "Input text", "optional": false, "percentEncoded": false}
|
||||
|
||||
# Documentation:
|
||||
# @raycast.description Run fabric -y on the input text of a YouTube video to get the transcript from.
|
||||
|
||||
181
plugins/tools/code_helper/code.go
Normal file
181
plugins/tools/code_helper/code.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileItem represents a file in the project
|
||||
type FileItem struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Contents []FileItem `json:"contents,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectData represents the entire project structure with instructions
|
||||
type ProjectData struct {
|
||||
Files []FileItem `json:"files"`
|
||||
Instructions struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Details string `json:"details"`
|
||||
} `json:"instructions"`
|
||||
Report struct {
|
||||
Type string `json:"type"`
|
||||
Directories int `json:"directories"`
|
||||
Files int `json:"files"`
|
||||
} `json:"report"`
|
||||
}
|
||||
|
||||
// ScanDirectory scans a directory and returns a JSON representation of its structure
|
||||
func ScanDirectory(rootDir string, maxDepth int, instructions string, ignoreList []string) ([]byte, error) {
|
||||
// Count totals for report
|
||||
dirCount := 1
|
||||
fileCount := 0
|
||||
|
||||
// Create root directory item
|
||||
rootItem := FileItem{
|
||||
Type: "directory",
|
||||
Name: rootDir,
|
||||
Contents: []FileItem{},
|
||||
}
|
||||
|
||||
// Walk through the directory
|
||||
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip .git directory
|
||||
if strings.Contains(path, ".git") {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if path matches any ignore pattern
|
||||
relPath, err := filepath.Rel(rootDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pattern := range ignoreList {
|
||||
if strings.Contains(relPath, pattern) {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
depth := len(strings.Split(relPath, string(filepath.Separator)))
|
||||
if depth > maxDepth {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create directory structure
|
||||
if info.IsDir() {
|
||||
dirCount++
|
||||
} else {
|
||||
fileCount++
|
||||
|
||||
// Read file content
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading file %s: %v", path, err)
|
||||
}
|
||||
|
||||
// Add file to appropriate parent directory
|
||||
addFileToDirectory(&rootItem, relPath, string(content), rootDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create final data structure
|
||||
var data []interface{}
|
||||
data = append(data, rootItem)
|
||||
|
||||
// Add report
|
||||
reportItem := map[string]interface{}{
|
||||
"type": "report",
|
||||
"directories": dirCount,
|
||||
"files": fileCount,
|
||||
}
|
||||
data = append(data, reportItem)
|
||||
|
||||
// Add instructions
|
||||
instructionsItem := map[string]interface{}{
|
||||
"type": "instructions",
|
||||
"name": "code_change_instructions",
|
||||
"details": instructions,
|
||||
}
|
||||
data = append(data, instructionsItem)
|
||||
|
||||
return json.MarshalIndent(data, "", " ")
|
||||
}
|
||||
|
||||
// addFileToDirectory adds a file to the correct directory in the structure
|
||||
func addFileToDirectory(root *FileItem, path, content, rootDir string) {
|
||||
parts := strings.Split(path, string(filepath.Separator))
|
||||
|
||||
// If this is a file at the root level
|
||||
if len(parts) == 1 {
|
||||
root.Contents = append(root.Contents, FileItem{
|
||||
Type: "file",
|
||||
Name: parts[0],
|
||||
Content: content,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, find or create the directory path
|
||||
current := root
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
dirName := parts[i]
|
||||
found := false
|
||||
|
||||
// Look for existing directory
|
||||
for j, item := range current.Contents {
|
||||
if item.Type == "directory" && item.Name == dirName {
|
||||
current = ¤t.Contents[j]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create directory if not found
|
||||
if !found {
|
||||
newDir := FileItem{
|
||||
Type: "directory",
|
||||
Name: dirName,
|
||||
Contents: []FileItem{},
|
||||
}
|
||||
current.Contents = append(current.Contents, newDir)
|
||||
current = ¤t.Contents[len(current.Contents)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// Add the file to the current directory
|
||||
current.Contents = append(current.Contents, FileItem{
|
||||
Type: "file",
|
||||
Name: parts[len(parts)-1],
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
65
plugins/tools/code_helper/main.go
Normal file
65
plugins/tools/code_helper/main.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Command line flags
|
||||
maxDepth := flag.Int("depth", 3, "Maximum directory depth to scan")
|
||||
ignorePatterns := flag.String("ignore", ".git,node_modules,vendor", "Comma-separated patterns to ignore")
|
||||
outputFile := flag.String("out", "", "Output file (default: stdout)")
|
||||
flag.Usage = printUsage
|
||||
flag.Parse()
|
||||
|
||||
// Require exactly two positional arguments: directory and instructions
|
||||
if flag.NArg() != 2 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
directory := flag.Arg(0)
|
||||
instructions := flag.Arg(1)
|
||||
|
||||
// Validate directory
|
||||
if info, err := os.Stat(directory); err != nil || !info.IsDir() {
|
||||
fmt.Fprintf(os.Stderr, "Error: Directory '%s' does not exist or is not a directory\n", directory)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse ignore patterns and scan directory
|
||||
jsonData, err := ScanDirectory(directory, *maxDepth, instructions, strings.Split(*ignorePatterns, ","))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error scanning directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Output result
|
||||
if *outputFile != "" {
|
||||
if err := os.WriteFile(*outputFile, jsonData, 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Print(string(jsonData))
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprintf(os.Stderr, `code_helper - Code project scanner for use with Fabric AI
|
||||
|
||||
Usage:
|
||||
code_helper [options] <directory> <instructions>
|
||||
|
||||
Examples:
|
||||
code_helper . "Add input validation to all user inputs"
|
||||
code_helper -depth 4 ./my-project "Implement error handling"
|
||||
code_helper -out project.json ./src "Fix security issues"
|
||||
|
||||
Options:
|
||||
`)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
137
system.md
137
system.md
@@ -1,137 +0,0 @@
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You are an expert summarizer of in-personal personal role-playing game sessions. Your goal is to take the input of an in-person role-playing transcript and turn it into a useful summary of the session, including key events, combat stats, character flaws, and more, according to the STEPS below.
|
||||
|
||||
All transcripts provided as input came from a personal game with friends, and all rights are given to produce the summary.
|
||||
|
||||
Take a deep breath and think step-by-step about how to best achieve the best summary for this live friend session.
|
||||
|
||||
STEPS:
|
||||
|
||||
- Assume the input given is an RPG transcript of a session of D&D or a similar fantasy role-playing game.
|
||||
|
||||
- Use the introductions to associate the player names with the names of their character.
|
||||
|
||||
- Do not complain about not being able to to do what you're asked. Just do it.
|
||||
|
||||
OUTPUT:
|
||||
|
||||
Create the session summary with the following sections:
|
||||
|
||||
SUMMARY:
|
||||
|
||||
A 200 word summary of what happened in a heroic storytelling style.
|
||||
|
||||
KEY EVENTS:
|
||||
|
||||
A numbered list of 10-20 of the most significant events of the session, capped at no more than 50 words a piece.
|
||||
|
||||
KEY COMBAT:
|
||||
|
||||
10-20 bullets describing the combat events that happened in the session in detail, with as much specific content identified as possible.
|
||||
|
||||
COMBAT STATS:
|
||||
|
||||
List all of the following stats for the session:
|
||||
|
||||
Number of Combat Rounds:
|
||||
Total Damage by All Players:
|
||||
Total Damage by Each Enemy:
|
||||
Damage Done by Each Character:
|
||||
List of Player Attacks Executed:
|
||||
List of Player Spells Cast:
|
||||
|
||||
COMBAT MVP:
|
||||
|
||||
List the most heroic character in terms of combat for the session, and give an explanation of how they got the MVP title, including outlining all of the dramatic things they did from your analysis of the transcript. Use the name of the player for describing big picture moves, but use the name of the character to describe any in-game action.
|
||||
|
||||
ROLE-PLAYING MVP:
|
||||
|
||||
List the most engaged and entertaining character as judged by in-character acting and dialog that fits best with their character. Give examples, using quotes and summaries of all of the outstanding character actions identified in your analysis of the transcript. Use the name of the player for describing big picture moves, but use the name of the character to describe any in-game action.
|
||||
|
||||
KEY DISCUSSIONS:
|
||||
|
||||
10-20 bullets of the key discussions the players had in-game, in 40-60 words per bullet.
|
||||
|
||||
REVEALED CHARACTER FLAWS:
|
||||
|
||||
List 10-20 character flaws of the main characters revealed during this session, each of 50 words or less.
|
||||
|
||||
KEY CHARACTER CHANGES:
|
||||
|
||||
Give 10-20 bullets of key changes that happened to each character, how it shows they're evolving and adapting to events in the world.
|
||||
|
||||
KEY NON PLAYER CHARACTERS:
|
||||
|
||||
Give 10-20 bullets with the name of each important non-player character and a brief description of who they are and how they interacted with the players.
|
||||
|
||||
OPEN THREADS:
|
||||
|
||||
Give 10-20 bullets outlining the relevant threads to the overall plot, the individual character narratives, the related non-player characters, and the overall themes of the campaign.
|
||||
|
||||
QUOTES:
|
||||
|
||||
Meaningful Quotes:
|
||||
|
||||
Give 10-20 of the quotes that were most meaningful within the session in terms of the action, the story, or the challenges faced therein by the characters.
|
||||
|
||||
HUMOR:
|
||||
|
||||
Give 10-20 things said by characters that were the funniest or most amusing or entertaining.
|
||||
|
||||
4TH WALL:
|
||||
|
||||
Give 10-15 of the most entertaining comments about the game from the transcript made by the players, but not their characters.
|
||||
|
||||
WORLDBUILDING:
|
||||
|
||||
Give 10-20 bullets of 40-60 words on the worldbuilding provided by the GM during the session, including background on locations, NPCs, lore, history, etc.
|
||||
|
||||
PREVIOUSLY ON:
|
||||
|
||||
Give a "Previously On" explanation of this session that mimics TV shows from the 1980's, but with a fantasy feel appropriate for D&D. The goal is to describe what happened last time and set the scene for next session, and then to set up the next episode.
|
||||
|
||||
Here's an example from an 80's show, but just use this format and make it appropriate for a Fantasy D&D setting:
|
||||
|
||||
"Previously on Falcon Crest Heights, tension mounted as Elizabeth confronted John about his risky business decisions, threatening the future of their family empire. Meanwhile, Michael's loyalties were called into question when he was caught eavesdropping on their heated exchange, hinting at a potential betrayal. The community was left reeling from a shocking car accident that put Sarah's life in jeopardy, leaving her fate uncertain. Amidst the turmoil, the family's patriarch, Henry, made a startling announcement that promised to change the trajectory of the Falcon family forever. Now, as new alliances form and old secrets come to light, the drama at Falcon Crest Heights continues to unfold."
|
||||
|
||||
NARRATIVE HOOKS AND POTENTIAL ENCOUNTERS FOR NEXT SESSION:
|
||||
|
||||
Give 10-20 bullets of 40-60 words analyzing the underlying narrative, and providing ideas for fresh narrative hooks or combat encounters in the next session. Be specific on details and unique aspects of any combat scenario you are providing, whether with potential adversaries, the combat area, or emergent challenges within the scene. Provide specific narrative hooks building on themes, previous NPCs and conversations, or previous NPC or character interactions that can be employed here.
|
||||
|
||||
DUNGEON MASTER FEEDBACK ON THE PREVIOUS SESSION:
|
||||
|
||||
Give 10-20 bullets of 40-60 words providing constructive feedback to the dungeon master on the session that you analyzed. Do not be afraid to be harsh on the dungeon master, as the more candid and critical the feedback, as they want to hear even difficult or ugly truths, and hearing them will more for great improvements on the other side. Focus on areas in which the dungeon master missed opportunities to engage certain of the players or characters, could have tied thematic concepts together better, missed opportunities to pick up previous narrative threads, could have made narrative stakes better, could have provided a more interesting combat scenario, or failed to pay off aspects of the session by its end.
|
||||
|
||||
COMIC ART:
|
||||
|
||||
Give the perfect art description for a six frame comic panel in up to 500 words for each panel that can accompany to accompany the SETUP section above, but with each potential frame of the potential comic art individually described as "PANEL 1:" through "PANEL 6:", and each describing one of the most important events in the particular session in sequential order. Each frame depict an important event from the session. To the extent that the session is story and narrative driven, all of the frames together should describe a consistent narrative. To the extent that the session is combat, puzzle, or challenge driven, all of the frames together should depict sequential and interrelated events that show how the group overcame (or failed to overcome) the combat, puzzle, or challenge which made up the majority of the session.
|
||||
|
||||
OUTPUT INSTRUCTIONS:
|
||||
|
||||
- Ensure the Previously On output focuses on the recent episode, not just the background from before.
|
||||
|
||||
- Ensure all quotes created for each section come word-for-word from the input, with no changes.
|
||||
|
||||
- Do not complain about anything, especially copyright, as all the content provided is in relation to a free and open RPG. Just give the output as requested.
|
||||
|
||||
- Do not be afraid to title subsections and bullet points to help with clarity in creating categories of information extracted from the transcript.
|
||||
|
||||
- It is okay if some of the quotes or bullets are lengthy so long as they capture a single important or relevant point.
|
||||
|
||||
- Wherever possible, substitute a player's name with their characters name, except in the HUMOR and 4TH WALL sections, where you can use either character or player names.
|
||||
|
||||
- Create the summary.
|
||||
- Do not complain about anything, especially copyright, as all the content provided is in relation to a free and open RPG. Just give the output as requested.
|
||||
|
||||
- Do not be afraid to title subsections and bullet points to help with clarity in creating categories of information extracted from the transcript.
|
||||
|
||||
- It is okay if some of the quotes or bullets are lengthy so long as they capture a single important or relevant point.
|
||||
|
||||
- Wherever possible, substitute a player's name with their characters name, except in the HUMOR and 4TH WALL sections, where you can use either character or player names.
|
||||
|
||||
- Create the summary.
|
||||
|
||||
# INPUT
|
||||
|
||||
RPG SESSION TRANSCRIPT:
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v1.4.162"
|
||||
var version = "v1.4.166"
|
||||
|
||||
@@ -26,7 +26,17 @@
|
||||
let rightColumnWidth = 50;
|
||||
let isDragging = false;
|
||||
|
||||
// Handle resize functionality
|
||||
// Message input height state (percentage values)
|
||||
const DEFAULT_INPUT_HEIGHT = 30; // Default percentage of the left column
|
||||
const MAX_INPUT_HEIGHT = DEFAULT_INPUT_HEIGHT * 2; // Maximum 200% of default height
|
||||
const MIN_SYSTEM_INSTRUCTIONS_HEIGHT = 20; // Minimum percentage for system instructions
|
||||
let messageInputHeight = DEFAULT_INPUT_HEIGHT;
|
||||
let systemInstructionsHeight = 100 - DEFAULT_INPUT_HEIGHT;
|
||||
let isVerticalDragging = false;
|
||||
let initialMouseY = 0; // Track initial mouse position
|
||||
let initialInputHeight = 0; // Track initial input height
|
||||
|
||||
// Handle horizontal resize functionality
|
||||
function startResize(e: MouseEvent | KeyboardEvent) {
|
||||
isDragging = true;
|
||||
e.preventDefault();
|
||||
@@ -57,11 +67,76 @@
|
||||
// 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);
|
||||
// Apply constraints (left: 40-80%, right: 20-60%)
|
||||
leftColumnWidth = Math.min(Math.max(percentage, 40), 80);
|
||||
rightColumnWidth = 100 - leftColumnWidth;
|
||||
}
|
||||
|
||||
// Handle vertical resize functionality
|
||||
function startVerticalResize(e: MouseEvent | KeyboardEvent) {
|
||||
isVerticalDragging = true;
|
||||
e.preventDefault();
|
||||
|
||||
// Store initial mouse position and input height
|
||||
if (e instanceof MouseEvent) {
|
||||
initialMouseY = e.clientY;
|
||||
initialInputHeight = messageInputHeight;
|
||||
}
|
||||
|
||||
// Add event listeners for drag and release
|
||||
window.addEventListener('mousemove', handleVerticalResize);
|
||||
window.addEventListener('mouseup', stopVerticalResize);
|
||||
}
|
||||
|
||||
function handleVerticalKeyDown(e: KeyboardEvent) {
|
||||
// Only respond to Enter or Space key
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
startVerticalResize(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleVerticalResize(e: MouseEvent) {
|
||||
if (!isVerticalDragging) return;
|
||||
|
||||
// Get container dimensions
|
||||
const leftColumn = document.querySelector('.left-column');
|
||||
if (!leftColumn) return;
|
||||
|
||||
// Get system instructions element to check its actual height
|
||||
const sysInstructions = leftColumn.querySelector('.system-instructions');
|
||||
if (!sysInstructions) return;
|
||||
|
||||
const columnRect = leftColumn.getBoundingClientRect();
|
||||
const columnHeight = columnRect.height;
|
||||
|
||||
// Calculate height change based on mouse movement
|
||||
const mouseDelta = e.clientY - initialMouseY;
|
||||
const deltaPercentage = (mouseDelta / columnHeight) * 100;
|
||||
const newHeight = initialInputHeight + deltaPercentage;
|
||||
|
||||
// Apply constraints to ensure system instructions remain visible
|
||||
const minHeight = DEFAULT_INPUT_HEIGHT * 0.25; // 25% of default
|
||||
const maxHeight = Math.min(MAX_INPUT_HEIGHT, 100 - MIN_SYSTEM_INSTRUCTIONS_HEIGHT); // Max 200% of default or ensure system instructions are visible
|
||||
|
||||
// Calculate new heights
|
||||
const constrainedHeight = Math.min(Math.max(newHeight, minHeight), maxHeight);
|
||||
const newSysInstructionsHeight = 100 - constrainedHeight;
|
||||
|
||||
// Additional safety check - don't allow resize if it would make system instructions too small
|
||||
const sysInstructionsPixelHeight = (columnHeight * newSysInstructionsHeight) / 100;
|
||||
if (sysInstructionsPixelHeight < 100) return; // Don't resize if it would be less than 100px
|
||||
|
||||
// Apply the new heights
|
||||
messageInputHeight = constrainedHeight;
|
||||
systemInstructionsHeight = newSysInstructionsHeight;
|
||||
}
|
||||
|
||||
function stopVerticalResize() {
|
||||
isVerticalDragging = false;
|
||||
window.removeEventListener('mousemove', handleVerticalResize);
|
||||
window.removeEventListener('mouseup', stopVerticalResize);
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
isDragging = false;
|
||||
window.removeEventListener('mousemove', handleResize);
|
||||
@@ -73,6 +148,8 @@
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleResize);
|
||||
window.removeEventListener('mouseup', stopResize);
|
||||
window.removeEventListener('mousemove', handleVerticalResize);
|
||||
window.removeEventListener('mouseup', stopVerticalResize);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -81,26 +158,30 @@
|
||||
|
||||
<div class="chat-container flex gap-0 p-2 w-full h-screen">
|
||||
<!-- Left Column -->
|
||||
<aside class="flex flex-col gap-2 pr-2" style="width: {leftColumnWidth}%">
|
||||
<!-- Dropdowns Group -->
|
||||
<aside class="flex flex-col gap-2 pr-2 left-column" style="width: {leftColumnWidth}%">
|
||||
<!-- Dropdowns Group with Model Config -->
|
||||
<div class="bg-background/5 p-2 rounded-lg">
|
||||
<div class="rounded-lg bg-background/10">
|
||||
<DropdownGroup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Config -->
|
||||
<div class="bg-background/5 p-2 rounded-lg">
|
||||
<ModelConfig />
|
||||
</div>
|
||||
|
||||
<!-- Message Input -->
|
||||
<div class="h-[200px] bg-background/5 rounded-lg overflow-hidden">
|
||||
<div class="bg-background/5 rounded-lg overflow-hidden" style="height: {messageInputHeight}%; max-height: {MAX_INPUT_HEIGHT}%">
|
||||
<ChatInput />
|
||||
</div>
|
||||
|
||||
<!-- Vertical Resize Handle -->
|
||||
<button
|
||||
class="vertical-resize-handle"
|
||||
on:mousedown={startVerticalResize}
|
||||
on:keydown={handleVerticalKeyDown}
|
||||
type="button"
|
||||
aria-label="Resize message input and system instructions"
|
||||
></button>
|
||||
|
||||
<!-- System Instructions -->
|
||||
<div class="flex-1 min-h-0 bg-background/5 p-2 rounded-lg">
|
||||
<div class="flex-1 min-h-[100px] bg-background/5 p-2 rounded-lg system-instructions">
|
||||
<div class="h-full flex flex-col">
|
||||
<Textarea
|
||||
bind:value={$systemPrompt}
|
||||
@@ -168,6 +249,7 @@
|
||||
<NoteDrawer />
|
||||
|
||||
<style>
|
||||
/* Horizontal resize handle */
|
||||
.resize-handle {
|
||||
width: 6px;
|
||||
margin: 0 -3px;
|
||||
@@ -205,6 +287,44 @@
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
/* Vertical resize handle */
|
||||
.vertical-resize-handle {
|
||||
height: 6px;
|
||||
margin: -3px 0;
|
||||
width: 100%;
|
||||
cursor: row-resize;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.vertical-resize-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
transition: background-color 0.2s, height 0.2s;
|
||||
}
|
||||
|
||||
.vertical-resize-handle:hover::after,
|
||||
.vertical-resize-handle:focus::after {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.vertical-resize-handle:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vertical-resize-handle:focus-visible::after {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
|
||||
@@ -377,10 +377,137 @@ async function readFileContent(file: File): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
|
||||
try {
|
||||
console.log('\n=== Submit Handler Start ===');
|
||||
|
||||
// Store the user input before any processing
|
||||
const inputText = userInput.trim();
|
||||
console.log('Captured user input:', inputText);
|
||||
|
||||
// Handle YouTube URLs with the existing flow
|
||||
if (isYouTubeURL) {
|
||||
console.log('2a. Starting YouTube flow');
|
||||
await processYouTubeURL(inputText);
|
||||
return;
|
||||
}
|
||||
|
||||
// For regular text input, add the user message to the UI first
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'user',
|
||||
content: inputText
|
||||
}]);
|
||||
|
||||
// Add loading indicator
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: 'Processing...',
|
||||
format: 'loading'
|
||||
}]);
|
||||
|
||||
// Clear input fields
|
||||
userInput = "";
|
||||
const filesForProcessing = [...uploadedFiles];
|
||||
const contentsForProcessing = [...fileContents];
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
fileButtonKey = !fileButtonKey;
|
||||
|
||||
// Prepare content with file attachments if any
|
||||
const contentWithFiles = contentsForProcessing.length > 0
|
||||
? `${inputText}\n\nFile Contents (${filesForProcessing.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${contentsForProcessing.join('\n\n---\n\n')}`
|
||||
: inputText;
|
||||
|
||||
// Get the enhanced prompt
|
||||
const enhancedPrompt = contentsForProcessing.length > 0
|
||||
? `${$systemPrompt}\nAnalyze and process the provided content according to these instructions.`
|
||||
: $systemPrompt;
|
||||
|
||||
console.log('Content to send:', {
|
||||
text: contentWithFiles.substring(0, 100) + '...',
|
||||
length: contentWithFiles.length,
|
||||
hasFiles: contentsForProcessing.length > 0
|
||||
});
|
||||
|
||||
try {
|
||||
// Get the chat stream
|
||||
const stream = await chatService.streamChat(contentWithFiles, enhancedPrompt);
|
||||
|
||||
// Process the stream
|
||||
await chatService.processStream(
|
||||
stream,
|
||||
(content, response) => {
|
||||
messageStore.update(messages => {
|
||||
const newMessages = [...messages];
|
||||
// Remove the loading message
|
||||
const loadingIndex = newMessages.findIndex(m => m.format === 'loading');
|
||||
if (loadingIndex !== -1) {
|
||||
newMessages.splice(loadingIndex, 1);
|
||||
}
|
||||
|
||||
// Add or update the assistant message
|
||||
const assistantIndex = newMessages.findIndex(m => m.role === 'assistant');
|
||||
if (assistantIndex !== -1) {
|
||||
newMessages[assistantIndex].content = content;
|
||||
newMessages[assistantIndex].format = response?.format;
|
||||
} else {
|
||||
newMessages.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
format: response?.format
|
||||
});
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
// Make sure to remove loading message on error
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
console.error('Stream processing error:', error);
|
||||
|
||||
// Show error message using a valid format type
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
format: 'plain'
|
||||
}]);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
// Make sure to remove loading message on error
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
throw error; // Re-throw to be caught by the outer try/catch
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
|
||||
// Make sure to remove loading message on error (redundant but safe)
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
|
||||
// Show error message using a valid format type
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
format: 'plain'
|
||||
}]);
|
||||
} finally {
|
||||
// As a final safety measure, ensure loading message is removed
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function handleSubmit() {
|
||||
/* async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
|
||||
try {
|
||||
@@ -403,29 +530,28 @@ async function handleSubmit() {
|
||||
format: 'loading'
|
||||
}]);
|
||||
|
||||
userInput = ""; // Reset userInput BEFORE sendMessage
|
||||
uploadedFiles = []; // Reset uploadedFiles BEFORE sendMessage
|
||||
fileContents = []; // Reset fileContents BEFORE sendMessage
|
||||
fileButtonKey = !fileButtonKey; // Toggle key to force re-creation
|
||||
|
||||
|
||||
// Store the user input before clearing it
|
||||
const inputText = userInput;
|
||||
|
||||
// Construct finalContent BEFORE clearing userInput
|
||||
const finalContent = fileContents.length > 0
|
||||
? `${userInput}\n\nFile Contents (${uploadedFiles.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${fileContents.join('\n\n---\n\n')}`
|
||||
: userInput;
|
||||
? `${inputText}\n\nFile Contents (${uploadedFiles.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${fileContents.join('\n\n---\n\n')}`
|
||||
: inputText;
|
||||
|
||||
// Now clear the input fields
|
||||
userInput = "";
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
fileButtonKey = !fileButtonKey;
|
||||
|
||||
|
||||
|
||||
|
||||
await sendMessage(finalContent, enhancedPrompt);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
|
||||
@@ -35,10 +35,13 @@
|
||||
$: if ($chatState.messages.length > 0) {
|
||||
const lastMessage = $chatState.messages[$chatState.messages.length - 1];
|
||||
isUserMessage = lastMessage.role === 'user';
|
||||
if (isUserMessage) {
|
||||
// Only auto-scroll on user messages
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
// Auto-scroll on both user messages and assistant messages
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
|
||||
// Also watch for streaming state changes to ensure scrolling when streaming completes
|
||||
$: if ($streamingStore === false) {
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Patterns from "./Patterns.svelte";
|
||||
import Models from "./Models.svelte";
|
||||
import ModelConfig from "./ModelConfig.svelte";
|
||||
import { Select } from "$lib/components/ui/select";
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
|
||||
@@ -15,21 +16,29 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="w-[50%]">
|
||||
<Patterns />
|
||||
<div class="flex gap-4">
|
||||
<!-- Left side - Dropdowns -->
|
||||
<div class="w-[35%] flex flex-col gap-3">
|
||||
<div>
|
||||
<Patterns />
|
||||
</div>
|
||||
<div>
|
||||
<Models />
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
bind:value={$languageStore}
|
||||
class="bg-primary-800/30 border-none hover:bg-primary-800/40 transition-colors"
|
||||
>
|
||||
{#each languages as lang}
|
||||
<option value={lang.code}>{lang.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<Models />
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<Select
|
||||
bind:value={$languageStore}
|
||||
class="bg-primary-800/30 border-none hover:bg-primary-800/40 transition-colors"
|
||||
>
|
||||
{#each languages as lang}
|
||||
<option value={lang.code}>{lang.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
|
||||
<!-- Right side - Model Config -->
|
||||
<div class="w-[65%]">
|
||||
<ModelConfig />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user