Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions Dockerfile

This file was deleted.

6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
local-build:
go build -o panda
build:
go build -o panda --tags "fts5"

local-run: local-build
run: build
-./panda
rm ./panda
27 changes: 0 additions & 27 deletions docker-compose.yml

This file was deleted.

8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config

Check failure on line 1 in internal/config/config.go

View workflow job for this annotation

GitHub Actions / lint-build-test

: cannot compile Go 1.23 code (typecheck)

import (
"encoding/json"
Expand Down Expand Up @@ -31,6 +31,14 @@
return filepath.Join(configDir, appConfigDir)
}

func GetDataDir() string {
dataDir := xdg.DataHome
if dataDir == "" {
dataDir = filepath.Join(xdg.Home, ".local", "share")
}
return filepath.Join(dataDir, appConfigDir)
}

func GetFilePath() string {
configDir := GetDir()
return filepath.Join(configDir, configFileName)
Expand Down
107 changes: 102 additions & 5 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import (
"fmt"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"os"
"path/filepath"

"github.com/aavshr/panda/internal/utils"

Check failure on line 8 in internal/db/db.go

View workflow job for this annotation

GitHub Actions / lint-build-test

could not import github.com/aavshr/panda/internal/utils (-: cannot compile Go 1.23 code) (typecheck)
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)

type Config struct {
Expand All @@ -18,11 +20,10 @@
}

func New(config Config, schemaInit, migrations *string) (*Store, error) {
// 0755 = rwxr-xr-x
if err := os.MkdirAll(config.DataDirPath, 0755); err != nil {
return nil, fmt.Errorf("could not make data dir, os.MkdirAll: %w", err)
}
f, err := os.Create(filepath.Join(config.DataDirPath, config.DatabaseName))
f, err := os.OpenFile(filepath.Join(config.DataDirPath, config.DatabaseName), os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return nil, fmt.Errorf("could not create database file, os.OpenFile: %w", err)
}
Expand Down Expand Up @@ -109,6 +110,29 @@
return nil
}

func (s *Store) CreateMessage(message *Message) error {
// TODO: is this implicit behavior okay?
if message.ID == "" {
messageID, err := utils.RandomID()
if err != nil {
return fmt.Errorf("could not generate random id, utils.RandomID: %w", err)
}
message.ID = messageID
}
tx, err := s.db.Beginx()
if err != nil {
return fmt.Errorf("could not start transaction, db.Beginx: %w", err)
}
if err := s.CreateMessageTx(tx, message); err != nil {
tx.Rollback()
return fmt.Errorf("could not create message, CreateMessageTx: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("could not commit transaction, tx.Commit: %w", err)
}
return nil
}

func (s *Store) CreateMessageTx(tx *sqlx.Tx, message *Message) error {
query := `INSERT INTO messages (id, m_role, content, created_at, thread_id)
VALUES (:id, :m_role, :content, :created_at, :thread_id)`
Expand All @@ -124,7 +148,7 @@

func (s *Store) ListMessagesByThreadIDPaginated(threadID string, offset, limit int) ([]*Message, error) {
var messages []*Message
err := s.db.Select(&messages, "SELECT * FROM messages WHERE thread_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", threadID, limit, offset)
err := s.db.Select(&messages, "SELECT * FROM messages WHERE thread_id = $1 ORDER BY created_at LIMIT $2 OFFSET $3", threadID, limit, offset)
if err != nil {
return nil, fmt.Errorf("could not select messages, db.Select: %w", err)
}
Expand All @@ -148,3 +172,76 @@
}
return threads, nil
}

func (s *Store) UpsertThread(thread *Thread) error {
tx, err := s.db.Beginx()
if err != nil {
return fmt.Errorf("could not start transaction, db.Beginx: %w", err)
}
if err := s.UpsertThreadTx(tx, thread); err != nil {
tx.Rollback()
return fmt.Errorf("could not upsert thread, UpsertThreadTx: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("could not commit transaction, tx.Commit: %w", err)
}
return nil
}

func (s *Store) UpsertThreadTx(tx *sqlx.Tx, thread *Thread) error {
query := `INSERT INTO threads (id, t_name, created_at, updated_at, external_message_store)
VALUES (:id, :t_name, :created_at, :updated_at, :external_message_store)
ON CONFLICT(id) DO UPDATE SET t_name = :t_name, updated_at = :updated_at`
if _, err := tx.NamedExec(query, thread); err != nil {
return fmt.Errorf("tx.NamedExec: %w", err)
}
query = `DELETE FROM virtual_thread_names WHERE thread_id = $1`
if _, err := tx.Exec(query, thread.ID); err != nil {
return fmt.Errorf("tx.Exec: %w", err)
}
query = `INSERT INTO virtual_thread_names (thread_id, thread_name) VALUES ($1, $2)`
if _, err := tx.Exec(query, thread.ID, thread.Name); err != nil {
return fmt.Errorf("tx.Exec: %w", err)
}
return nil
}

func (s *Store) DeleteThread(id string) error {
tx, err := s.db.Beginx()
if err != nil {
return fmt.Errorf("could not start transaction, db.Beginx: %w", err)
}
if err := s.DeleteThreadTx(tx, id); err != nil {
tx.Rollback()
return fmt.Errorf("could not delete thread, DeleteThreadTx: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("could not commit transaction, tx.Commit: %w", err)
}
return nil
}

func (s *Store) DeleteAllThreads() error {
tx, err := s.db.Beginx()
if err != nil {
return fmt.Errorf("could not start transaction, db.Beginx: %w", err)
}
query := `DELETE FROM threads`
if _, err := tx.Exec(query); err != nil {
return fmt.Errorf("could not delete threads, db.Exec: %w", err)
}
query = `DELETE FROM virtual_thread_names`
if _, err := tx.Exec(query); err != nil {
tx.Rollback()
return fmt.Errorf("could not delete virtual thread names, db.Exec: %w", err)
}
query = `DELETE FROM virtual_message_content`
if _, err := tx.Exec(query); err != nil {
tx.Rollback()
return fmt.Errorf("could not delete virtual message content, db.Exec: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("could not commit transaction, tx.Commit: %w", err)
}
return nil
}
6 changes: 3 additions & 3 deletions internal/db/schema/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ CREATE TABLE IF NOT EXISTS messages (
thread_id TEXT REFERENCES threads(id) ON DELETE CASCADE
);

CREATE VIRTUAL TABLE virtual_thread_names USING fts5(
CREATE VIRTUAL TABLE IF NOT EXISTS virtual_thread_names USING fts5(
thread_name,
thread_id UNINDEXED
);

CREATE VIRTUAL TABLE virtual_message_content USING fts5(
CREATE VIRTUAL TABLE IF NOT EXISTS virtual_message_content USING fts5(
message_content,
message_id UNINDEXED,
thread_id UNINDEXED
);
);
14 changes: 3 additions & 11 deletions internal/ui/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
"slices"
"time"

"github.com/aavshr/panda/internal/config"

Check failure on line 11 in internal/ui/handlers.go

View workflow job for this annotation

GitHub Actions / lint-build-test

could not import github.com/aavshr/panda/internal/config (-: cannot compile Go 1.23 code) (typecheck)
"github.com/aavshr/panda/internal/db"
"github.com/aavshr/panda/internal/ui/components"
"github.com/aavshr/panda/internal/ui/styles"

Check failure on line 14 in internal/ui/handlers.go

View workflow job for this annotation

GitHub Actions / lint-build-test

could not import github.com/aavshr/panda/internal/ui/styles (-: cannot compile Go 1.23 code) (typecheck)
"github.com/aavshr/panda/internal/utils"

Check failure on line 15 in internal/ui/handlers.go

View workflow job for this annotation

GitHub Actions / lint-build-test

could not import github.com/aavshr/panda/internal/utils (-: cannot compile Go 1.23 code) (typecheck)
tea "github.com/charmbracelet/bubbletea"
)

Expand Down Expand Up @@ -179,19 +179,11 @@
}

func (m *Model) handleListEnterMsg(msg components.ListEnterMsg) tea.Cmd {
// TODO: handle entering a message for messages
// either copy the entire message or let user copy only specific parts
switch m.focusedComponent {
case components.ComponentHistory:
// first item is always for new thread
if msg.Index == 0 {
m.setMessages([]*db.Message{})
m.setSelectedComponent(components.ComponentChatInput)
m.setFocusedComponent(components.ComponentChatInput)
return nil
}
m.setSelectedComponent(components.ComponentMessages)
m.setFocusedComponent(components.ComponentMessages)
m.setSelectedComponent(components.ComponentChatInput)
m.setFocusedComponent(components.ComponentChatInput)
return nil
}
return nil
}
Expand Down
3 changes: 1 addition & 2 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,8 @@
if err != nil {
return m, fmt.Errorf("store.ListLatestThreadsPaginated %w", err)
}
m.threads = append(m.threads, threads...)

m.messages = []*db.Message{}

m.historyModel = components.NewListModel(&components.NewListModelInput{
Title: titleHistory,
Items: components.NewThreadListItems(m.threads),
Expand All @@ -133,12 +131,13 @@
Delegate: components.NewThreadListItemDelegate(),
AllowInfiniteScrolling: false,
})
m.setThreads(append(m.threads, threads...))
m.historyModel.Select(0) // New Thread is selected by default
m.messagesModel = components.NewChatModel(conf.messagesWidth, conf.messagesHeight)
m.chatInputModel = components.NewChatInputModel(conf.chatInputWidth, conf.chatInputHeight)

listContainer := styles.ListContainerStyle()
historyContainer := listContainer.Copy().

Check failure on line 140 in internal/ui/model.go

View workflow job for this annotation

GitHub Actions / lint-build-test

declared and not used: historyContainer (typecheck)
Width(m.conf.historyWidth).
Height(m.conf.historyHeight)
messagesContainer := listContainer.Copy().
Expand Down
1 change: 0 additions & 1 deletion internal/ui/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ type Store interface {
ListLatestThreadsPaginated(offset, limit int) ([]*db.Thread, error)
ListMessagesByThreadIDPaginated(threadID string, offset, limit int) ([]*db.Message, error)
UpsertThread(thread *db.Thread) error
UpdateThreadName(threadID, name string) error
DeleteThread(threadID string) error
DeleteAllThreads() error
CreateMessage(message *db.Message) error
Expand Down
65 changes: 32 additions & 33 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
"log"
"os"

"strings"

"github.com/aavshr/panda/internal/config"

Check failure on line 10 in main.go

View workflow job for this annotation

GitHub Actions / lint-build-test

could not import github.com/aavshr/panda/internal/config (-: cannot compile Go 1.23 code) (typecheck)
"github.com/aavshr/panda/internal/db"
"github.com/aavshr/panda/internal/llm/openai"
"github.com/aavshr/panda/internal/ui"
//"github.com/aavshr/panda/internal/ui/llm"
"github.com/aavshr/panda/internal/ui/store"
tea "github.com/charmbracelet/bubbletea"
"golang.org/x/term"
//"log"
//"os"
//"strings"
)

//go:embed internal/db/schema/init.sql
Expand All @@ -24,35 +23,10 @@
var dbSchemaMigrations string

const (
DefaultDataDirPath = "/.local/share/panda/data"
DefaultDatabaseName = "panda.db"
)

func main() {
/*
isDev := strings.ToLower(os.Getenv("PANDA_ENV")) == "dev"
dataDirPath := DefaultDataDirPath
databaseName := DefaultDatabaseName
if isDev {
devDataDirPath := os.Getenv("PANDA_DATA_DIR_PATH")
devDatabaseName := os.Getenv("PANDA_DATABASE_NAME")
if devDataDirPath != "" {
dataDirPath = devDataDirPath
}
if devDatabaseName != "" {
databaseName = devDatabaseName
}
}

_, err := db.New(db.Config{
DataDirPath: dataDirPath,
DatabaseName: databaseName,
}, &dbSchemaInit, &dbSchemaMigrations)
if err != nil {
log.Fatal("failed to initialize db: ", err)
}
*/

func initMockStore() *store.Mock {
testThreads := []*db.Thread{
{
ID: "1",
Expand Down Expand Up @@ -98,8 +72,32 @@
},
}

mockStore := store.NewMock(testThreads, testMessages)
//mockLLM := llm.NewMock()
return store.NewMock(testThreads, testMessages)
}

func main() {
isDev := strings.ToLower(os.Getenv("PANDA_ENV")) == "dev"
dataDirPath := config.GetDataDir()
databaseName := DefaultDatabaseName
if isDev {
devDataDirPath := os.Getenv("PANDA_DATA_DIR_PATH")
devDatabaseName := os.Getenv("PANDA_DATABASE_NAME")
if devDataDirPath != "" {
dataDirPath = devDataDirPath
}
if devDatabaseName != "" {
databaseName = devDatabaseName
}
}

dbStore, err := db.New(db.Config{
DataDirPath: dataDirPath,
DatabaseName: databaseName,
}, &dbSchemaInit, &dbSchemaMigrations)
if err != nil {
log.Fatal("failed to initialize db: ", err)
}

openaiLLM := openai.New("")

width, height, err := term.GetSize(int(os.Stdout.Fd()))
Expand All @@ -110,9 +108,10 @@
m, err := ui.New(&ui.Config{
InitThreadsLimit: 10,
MaxThreadsLimit: 100,
MessagesLimit: 50,
Width: width - 8,
Height: height - 10,
}, mockStore, openaiLLM)
}, dbStore, openaiLLM)
if err != nil {
log.Fatal("ui.New: ", err)
}
Expand Down
Loading