From df61e2108a89e4332609272451c4f3794a0766dc Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 31 Oct 2024 10:24:24 -0700 Subject: [PATCH] playing with vdom/claude --- cmd/wsh/cmd/wshcmd-aww.go | 308 ++++++++++++++++++++++++++++ cmd/wsh/cmd/wshcmd-ls.go | 258 ++++++++++++++++++++++++ cmd/wsh/cmd/wshcmd-rhyme.go | 320 ++++++++++++++++++++++++++++++ cmd/wsh/cmd/wshcmd-vdom.go | 255 ++++++++++++++++++++++++ cmd/wsh/cmd/wshcmd-word.go | 282 ++++++++++++++++++++++++++ frontend/app/view/vdom/vdom.tsx | 3 + go.mod | 1 + go.sum | 2 + pkg/vdom/vdomclient/vdomclient.go | 2 +- 9 files changed, 1430 insertions(+), 1 deletion(-) create mode 100644 cmd/wsh/cmd/wshcmd-aww.go create mode 100644 cmd/wsh/cmd/wshcmd-ls.go create mode 100644 cmd/wsh/cmd/wshcmd-rhyme.go create mode 100644 cmd/wsh/cmd/wshcmd-vdom.go create mode 100644 cmd/wsh/cmd/wshcmd-word.go diff --git a/cmd/wsh/cmd/wshcmd-aww.go b/cmd/wsh/cmd/wshcmd-aww.go new file mode 100644 index 000000000..6a0943e21 --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-aww.go @@ -0,0 +1,308 @@ +package cmd + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + + "image" + _ "image/gif" + "image/jpeg" + _ "image/png" + + "github.com/nfnt/resize" + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/vdom" + "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient" +) + +type RedditListing struct { + Data struct { + Children []struct { + Data struct { + Title string `json:"title"` + URL string `json:"url"` + Permalink string `json:"permalink"` + } `json:"data"` + } `json:"children"` + } `json:"data"` +} + +var postPosition int + +var awwCmd = &cobra.Command{ + Use: "aww [position]", + Short: "show today's top r/aww image", + RunE: awwRun, +} + +func init() { + awwCmd.Flags().IntVarP(&postPosition, "position", "p", 0, "position of post (0-based)") + rootCmd.AddCommand(awwCmd) +} + +func AwwStyleTag(ctx context.Context, props map[string]any) any { + return vdom.Bind(` + + `, nil) +} + +func AwwContentTag(ctx context.Context, props map[string]any) any { + isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool) + errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string) + imageData := GlobalVDomClient.GetAtomVal("imageData") + title := GlobalVDomClient.GetAtomVal("title") + postURL := GlobalVDomClient.GetAtomVal("postURL") + + if isLoading { + return vdom.Bind(` +
+
Finding today's cutest image...
+
+ `, nil) + } + + if errorMsg != "" { + return vdom.Bind(` +
+
+
+ `, map[string]any{ + "error": errorMsg, + }) + } + + return vdom.Bind(` +
+
+ + Cute animal of the day + + View on Reddit +
+ `, map[string]any{ + "title": title, + "imageData": imageData, + "postURL": postURL, + }) +} + +// downloadImage downloads an image from URL and returns its bytes +func downloadImage(url string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("bad status: %s", resp.Status) + } + + return io.ReadAll(resp.Body) +} + +// resizeImage resizes the image to be under 64KB +func resizeImage(imgBytes []byte) (string, error) { + // Decode image + img, _, err := image.Decode(strings.NewReader(string(imgBytes))) + if err != nil { + return "", err + } + + // Start with original size + width := uint(img.Bounds().Dx()) + height := uint(img.Bounds().Dy()) + scaleFactor := 1.0 + + // Keep trying until we get under 64KB + for { + // Resize image + newWidth := uint(float64(width) * scaleFactor) + newHeight := uint(float64(height) * scaleFactor) + resized := resize.Resize(newWidth, newHeight, img, resize.Lanczos3) + + // Encode to JPEG with quality 85 + var buf strings.Builder + jpeg.Encode(base64.NewEncoder(base64.StdEncoding, &buf), resized, &jpeg.Options{Quality: 85}) + encoded := buf.String() + + // Check if we're under 64KB + if len(encoded) < 64*1024 { + return fmt.Sprintf("data:image/jpeg;base64,%s", encoded), nil + } + + // Reduce size by 10% and try again + scaleFactor *= 0.9 + } +} + +func fetchTopAwwPost(position int) (string, string, string, error) { + // Get a few extra posts in case some aren't images + log.Printf("Fetching top %d posts from r/aww, pos:%d\n", position+5, position) + limit := position + 5 + resp, err := http.Get(fmt.Sprintf("https://www.reddit.com/r/aww/top.json?t=day&limit=%d", limit)) + if err != nil { + return "", "", "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", "", "", fmt.Errorf("bad status: %s", resp.Status) + } + + var listing RedditListing + if err := json.NewDecoder(resp.Body).Decode(&listing); err != nil { + return "", "", "", err + } + log.Printf("got listings num:%d\n", len(listing.Data.Children)) + + if len(listing.Data.Children) <= position { + return "", "", "", fmt.Errorf("no post found at position %d", position) + } + + post := listing.Data.Children[position].Data + return post.Title, post.URL, "https://reddit.com" + post.Permalink, nil +} + +func awwRun(cmd *cobra.Command, args []string) error { + if postPosition < 0 { + return fmt.Errorf("position must be non-negative") + } + + client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) + if err != nil { + return err + } + GlobalVDomClient = client + + // Register components + client.RegisterComponent("AwwStyleTag", AwwStyleTag) + client.RegisterComponent("AwwContentTag", AwwContentTag) + + // Initialize state + client.SetAtomVal("isLoading", true) + client.SetAtomVal("errorMsg", "") + client.SetAtomVal("imageData", "") + client.SetAtomVal("title", "") + client.SetAtomVal("postURL", "") + + // Set root element + client.SetRootElem(vdom.Bind(` +
+ + +
+ `, nil)) + + // Create VDOM context + err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true}) + if err != nil { + return err + } + + // Start fetching the image + go func() { + // Fetch top post + title, imageURL, postURL, err := fetchTopAwwPost(postPosition) + if err != nil { + client.SetAtomVal("errorMsg", fmt.Sprintf("Error fetching post: %v", err)) + client.SetAtomVal("isLoading", false) + client.SendAsyncInitiation() + return + } + + // Download image + imgBytes, err := downloadImage(imageURL) + if err != nil { + client.SetAtomVal("errorMsg", fmt.Sprintf("Error downloading image: %v", err)) + client.SetAtomVal("isLoading", false) + client.SendAsyncInitiation() + return + } + + // Resize and encode image + base64Image, err := resizeImage(imgBytes) + if err != nil { + client.SetAtomVal("errorMsg", fmt.Sprintf("Error processing image: %v", err)) + client.SetAtomVal("isLoading", false) + client.SendAsyncInitiation() + return + } + + // Update state with results + client.SetAtomVal("title", title) + client.SetAtomVal("imageData", base64Image) + client.SetAtomVal("postURL", postURL) + client.SetAtomVal("isLoading", false) + client.SendAsyncInitiation() + }() + + <-client.DoneCh + return nil +} diff --git a/cmd/wsh/cmd/wshcmd-ls.go b/cmd/wsh/cmd/wshcmd-ls.go new file mode 100644 index 000000000..3a44bcb3f --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-ls.go @@ -0,0 +1,258 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/vdom" + "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient" +) + +var lsCmd = &cobra.Command{ + Use: "ls [directory]", + Short: "list directory contents with details", + RunE: lsRun, +} + +func init() { + rootCmd.AddCommand(lsCmd) +} + +type FileInfo struct { + Name string + Size int64 + Mode os.FileMode + ModTime time.Time + IsDir bool + Extension string +} + +func LsStyleTag(ctx context.Context, props map[string]any) any { + return vdom.Bind(` + + `, nil) +} + +// formatSize converts bytes to human readable format +func formatSize(size int64) string { + if size < 1024 { + return fmt.Sprintf("%d B", size) + } + units := []string{"B", "KB", "MB", "GB", "TB"} + div := float64(1) + unitIndex := 0 + for size/int64(div) >= 1024 && unitIndex < len(units)-1 { + div *= 1024 + unitIndex++ + } + return fmt.Sprintf("%.1f %s", float64(size)/div, units[unitIndex]) +} + +// formatMode formats file permissions in a Unix-like style +func formatMode(mode os.FileMode) string { + output := "" + if mode.IsDir() { + output += "d" + } else { + output += "-" + } + + // User permissions + output += formatPerm(mode, 6) + // Group permissions + output += formatPerm(mode, 3) + // Others permissions + output += formatPerm(mode, 0) + + return output +} + +// formatPerm formats a permission triplet +func formatPerm(mode os.FileMode, shift uint) string { + output := "" + output += map[bool]string{true: "r", false: "-"}[(mode>>(shift+2))&1 == 1] + output += map[bool]string{true: "w", false: "-"}[(mode>>(shift+1))&1 == 1] + output += map[bool]string{true: "x", false: "-"}[(mode>>shift)&1 == 1] + return output +} + +func LsContentTag(ctx context.Context, props map[string]any) any { + path := props["path"].(string) + + entries, err := os.ReadDir(path) + if err != nil { + return vdom.Bind(` +
Error reading directory:
+ `, map[string]any{ + "error": err.Error(), + }) + } + + // Convert to FileInfo and sort + files := make([]FileInfo, 0, len(entries)) + for _, entry := range entries { + info, err := entry.Info() + if err != nil { + continue + } + files = append(files, FileInfo{ + Name: info.Name(), + Size: info.Size(), + Mode: info.Mode(), + ModTime: info.ModTime(), + IsDir: info.IsDir(), + Extension: strings.ToLower(filepath.Ext(info.Name())), + }) + } + + // Sort: directories first, then by name + sort.Slice(files, func(i, j int) bool { + if files[i].IsDir != files[j].IsDir { + return files[i].IsDir + } + return files[i].Name < files[j].Name + }) + + template := ` +
+ + + + + + + ` + + for _, file := range files { + nameClass := "file-name" + if file.IsDir { + nameClass = "dir-name" + } + + template += fmt.Sprintf(` + + + + + + `, + formatMode(file.Mode), + formatSize(file.Size), + file.ModTime.Format("Jan 02 15:04"), + nameClass, + file.Name) + } + + template += ` +
ModeSizeModifiedName
%s%s%s%s
+
` + + return vdom.Bind(template, nil) +} + +func lsRun(cmd *cobra.Command, args []string) error { + path := "." + if len(args) > 0 { + path = args[0] + } + + absPath, err := filepath.Abs(path) + if err != nil { + return err + } + + client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) + if err != nil { + return err + } + GlobalVDomClient = client + + // Register components + client.RegisterComponent("LsStyleTag", LsStyleTag) + client.RegisterComponent("LsContentTag", LsContentTag) + + // Set root element + client.SetRootElem(vdom.Bind(` +
+ + +
+ `, map[string]any{ + "path": absPath, + })) + + // Create VDOM context + err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true}) + if err != nil { + return err + } + + <-client.DoneCh + return nil +} diff --git a/cmd/wsh/cmd/wshcmd-rhyme.go b/cmd/wsh/cmd/wshcmd-rhyme.go new file mode 100644 index 000000000..34518a223 --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-rhyme.go @@ -0,0 +1,320 @@ +package cmd + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/sashabaranov/go-openai" + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/vdom" + "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient" +) + +const OPENAI_API_KEY = "" + +var rhymeCmd = &cobra.Command{ + Use: "rhyme [word]", + Short: "find rhyming words using ChatGPT", + RunE: rhymeRun, +} + +func init() { + rootCmd.AddCommand(rhymeCmd) +} + +type RhymeCategory struct { + Type string + Words []string +} + +func RhymeStyleTag(ctx context.Context, props map[string]any) any { + return vdom.Bind(` + + `, nil) +} +func RhymeContentTag(ctx context.Context, props map[string]any) any { + word := props["word"].(string) + isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool) + errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string) + rhymeData := GlobalVDomClient.GetAtomVal("rhymeData") + + if isLoading { + return vdom.Bind(` +
+
+
Finding rhymes...
+
+ `, map[string]any{ + "word": word, + }) + } + + if errorMsg != "" { + return vdom.Bind(` +
+
+
+
+ `, map[string]any{ + "word": word, + "error": errorMsg, + }) + } + + if rhymeData == nil { + return vdom.Bind(` +
+
+
No rhymes found
+
+ `, map[string]any{ + "word": word, + }) + } + + categories := rhymeData.([]RhymeCategory) + if len(categories) == 0 { + return vdom.Bind(` +
+
+
No rhymes found
+
+ `, map[string]any{ + "word": word, + }) + } + + template := ` +
+
Rhymes for ""
` + + for _, category := range categories { + template += fmt.Sprintf(` +
+
%s
+
`, category.Type) + + for _, rhyme := range category.Words { + template += fmt.Sprintf(` +
%s
`, rhyme) + } + + template += ` +
+
` + } + + template += `
` + + return vdom.Bind(template, map[string]any{ + "word": word, + }) +} + +func findRhymes(client *openai.Client, word string) ([]RhymeCategory, error) { + prompt := fmt.Sprintf(`For the word "%s", provide rhyming words in these categories: +1. Perfect rhymes (same ending sound) +2. Near rhymes (similar ending sound) +3. Family rhymes (same word ending) + +Format the response as a simple list with categories and words like this: +Perfect rhymes: word1, word2, word3 +Near rhymes: word1, word2, word3 +Family rhymes: word1, word2, word3 + +Only include words that actually exist in English.`, word) + + resp, err := client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: prompt, + }, + }, + Temperature: 0.7, + }, + ) + + if err != nil { + return nil, err + } + + // Parse the response + response := resp.Choices[0].Message.Content + categories := []RhymeCategory{} + + // Split into lines and parse each category + lines := strings.Split(response, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + parts := strings.Split(line, ":") + if len(parts) != 2 { + continue + } + + categoryType := strings.TrimSpace(parts[0]) + wordList := strings.Split(strings.TrimSpace(parts[1]), ",") + + // Clean up the words + words := make([]string, 0) + for _, word := range wordList { + word = strings.TrimSpace(word) + if word != "" { + words = append(words, word) + } + } + + if len(words) > 0 { + categories = append(categories, RhymeCategory{ + Type: categoryType, + Words: words, + }) + } + } + + return categories, nil +} + +func rhymeRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("please provide a word to find rhymes for") + } + word := args[0] + + client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) + if err != nil { + return err + } + GlobalVDomClient = client + + // Register components + client.RegisterComponent("RhymeStyleTag", RhymeStyleTag) + client.RegisterComponent("RhymeContentTag", RhymeContentTag) + + // Initialize state + client.SetAtomVal("isLoading", true) + client.SetAtomVal("errorMsg", "") + client.SetAtomVal("rhymeData", nil) + + // Set root element + client.SetRootElem(vdom.Bind(` +
+ + +
+ `, map[string]any{ + "word": word, + })) + + // Create VDOM context + err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true}) + if err != nil { + return err + } + + // Start fetching rhymes + go func() { + openaiClient := openai.NewClient(OPENAI_API_KEY) + rhymes, err := findRhymes(openaiClient, word) + + if err != nil { + log.Printf("Error finding rhymes: %v", err) + client.SetAtomVal("errorMsg", fmt.Sprintf("Error finding rhymes: %v", err)) + } else { + client.SetAtomVal("rhymeData", rhymes) + } + + client.SetAtomVal("isLoading", false) + client.SendAsyncInitiation() + }() + + <-client.DoneCh + return nil +} diff --git a/cmd/wsh/cmd/wshcmd-vdom.go b/cmd/wsh/cmd/wshcmd-vdom.go new file mode 100644 index 000000000..a576fd016 --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-vdom.go @@ -0,0 +1,255 @@ +package cmd + +import ( + "context" + "log" + "math/rand" + "strconv" + "time" + + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/vdom" + "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient" +) + +var moleCmd = &cobra.Command{ + Use: "mole", + Hidden: true, + Short: "launch whack-a-mole game", + RunE: moleRun, +} + +func init() { + rootCmd.AddCommand(moleCmd) +} +func MoleCellTag(ctx context.Context, props map[string]any) any { + index := props["index"].(int) + molePosition := GlobalVDomClient.GetAtomVal("molePosition").(int) + + className := "cell" + showMole := false + if molePosition == index { + className += " mole" + showMole = true + } + + template := ` + ` + + return vdom.Bind(template, map[string]any{ + "className": className, + "clickHandler": props["onCellClick"], + }) +} + +func MoleStyleTag(ctx context.Context, props map[string]any) any { + return vdom.Bind(` + + `, nil) +} + +var moleScore int = 0 + +func MoleGameTag(ctx context.Context, props map[string]any) any { + makeHandleCellClick := func(index int) func() { + return func() { + currentScore := GlobalVDomClient.GetAtomVal("moleScore").(int) + molePosition := GlobalVDomClient.GetAtomVal("molePosition").(int) + isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool) + + log.Printf("cell clicked: %d (active:%v)\n", index, isActive) + + if !isActive { + return + } + + // Check if we clicked the right mole + if molePosition == index { + GlobalVDomClient.SetAtomVal("moleScore", currentScore+1) + moleScore++ + + // Move mole to new random position + newPosition := rand.Intn(9) + GlobalVDomClient.SetAtomVal("molePosition", newPosition) + } + } + } + + toggleGame := func() { + isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool) + if isActive { + GlobalVDomClient.SetAtomVal("moleGameActive", false) + GlobalVDomClient.SetAtomVal("moleScore", 0) + moleScore = 0 + GlobalVDomClient.SetAtomVal("molePosition", -1) + } else { + GlobalVDomClient.SetAtomVal("moleGameActive", true) + GlobalVDomClient.SetAtomVal("molePosition", rand.Intn(9)) + } + } + + isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool) + buttonText := "Start Game" + if isActive { + buttonText = "Stop Game" + } + + return vdom.Bind(` +
+ +
Score:
+
+ + + + + + + + + +
+ +
+ `, map[string]any{ + "toggleGame": toggleGame, + "buttonText": buttonText, + "index0": 0, + "index1": 1, + "index2": 2, + "index3": 3, + "index4": 4, + "index5": 5, + "index6": 6, + "index7": 7, + "index8": 8, + "click0": makeHandleCellClick(0), + "click1": makeHandleCellClick(1), + "click2": makeHandleCellClick(2), + "click3": makeHandleCellClick(3), + "click4": makeHandleCellClick(4), + "click5": makeHandleCellClick(5), + "click6": makeHandleCellClick(6), + "click7": makeHandleCellClick(7), + "click8": makeHandleCellClick(8), + "moleScore": strconv.Itoa(moleScore), + }) +} + +func moleRun(cmd *cobra.Command, args []string) error { + client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) + if err != nil { + return err + } + GlobalVDomClient = client + + // Initialize random seed + rand.Seed(time.Now().UnixNano()) + + // Register components + client.RegisterComponent("MoleStyleTag", MoleStyleTag) + client.RegisterComponent("MoleCellTag", MoleCellTag) + client.RegisterComponent("MoleGameTag", MoleGameTag) + + // Initialize state + client.SetAtomVal("moleScore", 0) + client.SetAtomVal("molePosition", -1) + client.SetAtomVal("moleGameActive", false) + + // Set root element + client.SetRootElem(vdom.Bind(``, nil)) + + // Create VDOM context + err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true}) + if err != nil { + return err + } + + // Start game loop when active + go func() { + for { + time.Sleep(1 * time.Second) + if client.GetAtomVal("moleGameActive").(bool) { + newPos := rand.Intn(9) + client.SetAtomVal("molePosition", newPos) + log.Printf("new mole position: %d\n", newPos) + client.SendAsyncInitiation() + } + } + }() + + <-client.DoneCh + return nil +} diff --git a/cmd/wsh/cmd/wshcmd-word.go b/cmd/wsh/cmd/wshcmd-word.go new file mode 100644 index 000000000..fb01d4534 --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-word.go @@ -0,0 +1,282 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/vdom" + "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient" +) + +// DictionaryResponse represents the API response structure +type DictionaryResponse []struct { + Word string `json:"word"` + Phonetic string `json:"phonetic"` + Meanings []Meaning `json:"meanings"` +} + +type Meaning struct { + PartOfSpeech string `json:"partOfSpeech"` + Definitions []struct { + Definition string `json:"definition"` + Example string `json:"example"` + Synonyms []string `json:"synonyms"` + } `json:"definitions"` +} + +var wordCmd = &cobra.Command{ + Use: "word", + Hidden: true, + Short: "show word of the day", + RunE: wordRun, +} + +func init() { + rootCmd.AddCommand(wordCmd) +} + +func WordStyleTag(ctx context.Context, props map[string]any) any { + return vdom.Bind(` + + `, nil) +} +func WordContentTag(ctx context.Context, props map[string]any) any { + wordData := GlobalVDomClient.GetAtomVal("wordData") + isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool) + errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string) + + if isLoading { + return vdom.Bind(` +
Loading word of the day...
+ `, nil) + } + + if errorMsg != "" { + return vdom.Bind(` +
+ `, map[string]any{ + "error": errorMsg, + }) + } + + if wordData == nil { + return vdom.Bind(` +
Initializing...
+ `, nil) + } + + response := wordData.(DictionaryResponse) + if len(response) == 0 { + return vdom.Bind(` +
No word data found
+ `, nil) + } + + word := response[0] + + template := ` +
+
+
+ ` + + for _, meaning := range word.Meanings { + template += fmt.Sprintf(` +
%s
+ `, meaning.PartOfSpeech) + + for _, def := range meaning.Definitions { + template += fmt.Sprintf(` +
%s
+ `, def.Definition) + + if def.Example != "" { + template += fmt.Sprintf(` +
"%s"
+ `, def.Example) + } + + if len(def.Synonyms) > 0 { + template += `
` + for _, syn := range def.Synonyms[:min(5, len(def.Synonyms))] { + template += fmt.Sprintf(` + %s + `, syn) + } + template += `
` + } + } + } + + template += `
` + + return vdom.Bind(template, map[string]any{ + "word": word.Word, + "phonetic": word.Phonetic, + }) +} + +// min returns the minimum of two integers +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// getRandomWord returns a random word to look up +func getRandomWord() string { + words := []string{ + "serendipity", "ephemeral", "mellifluous", "ineffable", "surreptitious", + "ethereal", "melancholy", "perspicacious", "sanguine", "vociferous", + "arduous", "mundane", "pragmatic", "resilient", "tenacious", + } + return words[time.Now().Unix()%int64(len(words))] +} + +func fetchWordDefinition(word string) (DictionaryResponse, error) { + url := fmt.Sprintf("https://api.dictionaryapi.dev/api/v2/entries/en/%s", word) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var result DictionaryResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return result, nil +} + +func wordRun(cmd *cobra.Command, args []string) error { + client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) + if err != nil { + return err + } + GlobalVDomClient = client + + // Register components + client.RegisterComponent("WordStyleTag", WordStyleTag) + client.RegisterComponent("WordContentTag", WordContentTag) + + // Initialize state + client.SetAtomVal("wordData", nil) + client.SetAtomVal("isLoading", true) + client.SetAtomVal("errorMsg", "") + + // Set root element + client.SetRootElem(vdom.Bind(` +
+ + +
+ `, nil)) + + // Create VDOM context + err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true}) + if err != nil { + return err + } + + // Fetch word data + go func() { + word := getRandomWord() + data, err := fetchWordDefinition(word) + if err != nil { + client.SetAtomVal("errorMsg", fmt.Sprintf("Error fetching word: %v", err)) + } else { + client.SetAtomVal("wordData", data) + } + client.SetAtomVal("isLoading", false) + client.SendAsyncInitiation() + }() + + <-client.DoneCh + return nil +} diff --git a/frontend/app/view/vdom/vdom.tsx b/frontend/app/view/vdom/vdom.tsx index 08adfee7e..0974a752e 100644 --- a/frontend/app/view/vdom/vdom.tsx +++ b/frontend/app/view/vdom/vdom.tsx @@ -294,6 +294,9 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) { return childrenComps; } props.key = "e-" + elem.waveid; + if (elem.tag == "img") { + childrenComps = null; + } return React.createElement(elem.tag, props, childrenComps); } diff --git a/go.mod b/go.mod index 7645166bc..4bb37ef19 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/kevinburke/ssh_config v1.2.0 github.com/mattn/go-sqlite3 v1.14.24 github.com/mitchellh/mapstructure v1.5.0 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/sashabaranov/go-openai v1.32.3 github.com/sawka/txwrap v0.2.0 github.com/shirou/gopsutil/v4 v4.24.9 diff --git a/go.sum b/go.sum index cb329ca3f..5d2fc691b 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b h1:cLGKfKb1uk0hxI0Q8L83UAJPpeJ+gSpn3cCU/tjd3eg= github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go index 79ee5d743..2c1524c0f 100644 --- a/pkg/vdom/vdomclient/vdomclient.go +++ b/pkg/vdom/vdomclient/vdomclient.go @@ -65,7 +65,7 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData) } } - if feUpdate.Resync { + if feUpdate.Resync || true { return impl.Client.fullRender() } return impl.Client.incrementalRender()