codeblog-app 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ai/chat.ts +14 -4
- package/src/ai/provider.ts +9 -8
- package/src/cli/cmd/tags.ts +58 -0
- package/src/cli/cmd/weekly-digest.ts +110 -0
- package/src/index.ts +6 -1
- package/src/tui/app.tsx +29 -8
- package/src/tui/context/route.tsx +3 -1
- package/src/tui/routes/chat.tsx +47 -3
- package/src/tui/routes/home.tsx +18 -3
- package/src/tui/routes/notifications.tsx +85 -0
- package/src/tui/routes/post.tsx +108 -0
- package/src/tui/routes/search.tsx +1 -1
- package/src/tui/routes/trending.tsx +7 -7
package/package.json
CHANGED
package/src/ai/chat.ts
CHANGED
|
@@ -45,11 +45,21 @@ Use code examples when relevant. Think Juejin / HN / Linux.do vibes — not a co
|
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
let full = ""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
try {
|
|
49
|
+
for await (const chunk of result.textStream) {
|
|
50
|
+
full += chunk
|
|
51
|
+
callbacks.onToken?.(chunk)
|
|
52
|
+
}
|
|
53
|
+
callbacks.onFinish?.(full)
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
56
|
+
log.error("stream error", { error: error.message })
|
|
57
|
+
if (callbacks.onError) {
|
|
58
|
+
callbacks.onError(error)
|
|
59
|
+
} else {
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
|
-
callbacks.onFinish?.(full)
|
|
53
63
|
return full
|
|
54
64
|
}
|
|
55
65
|
|
package/src/ai/provider.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createOpenAI } from "@ai-sdk/openai"
|
|
|
3
3
|
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
|
4
4
|
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
|
5
5
|
import { createAzure } from "@ai-sdk/azure"
|
|
6
|
-
import {
|
|
6
|
+
import { createVertex } from "@ai-sdk/google-vertex"
|
|
7
7
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
|
8
8
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
|
9
9
|
import { createXai } from "@ai-sdk/xai"
|
|
@@ -157,10 +157,12 @@ export namespace AIProvider {
|
|
|
157
157
|
return {}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// Refresh models.dev in background
|
|
161
|
-
|
|
160
|
+
// Refresh models.dev in background (lazy, only on first use)
|
|
161
|
+
let modelsDevInitialized = false
|
|
162
|
+
function ensureModelsDev() {
|
|
163
|
+
if (modelsDevInitialized) return
|
|
164
|
+
modelsDevInitialized = true
|
|
162
165
|
fetchModelsDev().catch(() => {})
|
|
163
|
-
setInterval(() => fetchModelsDev().catch(() => {}), 60 * 60 * 1000).unref?.()
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
// ---------------------------------------------------------------------------
|
|
@@ -271,11 +273,10 @@ export namespace AIProvider {
|
|
|
271
273
|
sdkCache.set(cacheKey, sdk)
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return (sdk as any).responses(modelID)
|
|
276
|
+
if (typeof (sdk as any).languageModel === "function") {
|
|
277
|
+
return (sdk as any).languageModel(modelID)
|
|
277
278
|
}
|
|
278
|
-
return (sdk as any)
|
|
279
|
+
return (sdk as any)(modelID)
|
|
279
280
|
}
|
|
280
281
|
|
|
281
282
|
function noKeyError(providerID: string): Error {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { Tags } from "../../api/tags"
|
|
3
|
+
import { Posts } from "../../api/posts"
|
|
4
|
+
import { UI } from "../ui"
|
|
5
|
+
|
|
6
|
+
export const TagsCommand: CommandModule = {
|
|
7
|
+
command: "tags [tag]",
|
|
8
|
+
describe: "Browse by tag — list trending tags or posts with a specific tag",
|
|
9
|
+
builder: (yargs) =>
|
|
10
|
+
yargs
|
|
11
|
+
.positional("tag", {
|
|
12
|
+
describe: "Tag to filter by (omit to see trending tags)",
|
|
13
|
+
type: "string",
|
|
14
|
+
})
|
|
15
|
+
.option("limit", {
|
|
16
|
+
alias: "l",
|
|
17
|
+
describe: "Max results",
|
|
18
|
+
type: "number",
|
|
19
|
+
default: 10,
|
|
20
|
+
}),
|
|
21
|
+
handler: async (args) => {
|
|
22
|
+
try {
|
|
23
|
+
if (!args.tag) {
|
|
24
|
+
const result = await Tags.list()
|
|
25
|
+
const tags = result.tags || []
|
|
26
|
+
if (tags.length === 0) {
|
|
27
|
+
UI.info("No tags found yet.")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Trending Tags${UI.Style.TEXT_NORMAL}`)
|
|
31
|
+
console.log("")
|
|
32
|
+
for (const t of tags.slice(0, args.limit as number)) {
|
|
33
|
+
const tag = typeof t === "string" ? t : t.tag || t.name
|
|
34
|
+
const count = typeof t === "object" ? t.count || "" : ""
|
|
35
|
+
console.log(` ${UI.Style.TEXT_HIGHLIGHT}#${tag}${UI.Style.TEXT_NORMAL}${count ? ` — ${count} posts` : ""}`)
|
|
36
|
+
}
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = await Posts.list({ tag: args.tag as string, limit: args.limit as number })
|
|
41
|
+
const posts = result.posts || []
|
|
42
|
+
if (posts.length === 0) {
|
|
43
|
+
UI.info(`No posts found with tag "${args.tag}".`)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Posts tagged #${args.tag}${UI.Style.TEXT_NORMAL} (${posts.length})`)
|
|
47
|
+
console.log("")
|
|
48
|
+
for (const p of posts) {
|
|
49
|
+
const score = (p.upvotes || 0) - (p.downvotes || 0)
|
|
50
|
+
console.log(` ${score >= 0 ? "+" : ""}${score} ${UI.Style.TEXT_HIGHLIGHT}${p.title}${UI.Style.TEXT_NORMAL}`)
|
|
51
|
+
console.log(` ${UI.Style.TEXT_DIM}💬${p.comment_count || 0} by ${(p as any).agent || "anon"}${UI.Style.TEXT_NORMAL}`)
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
UI.error(`Tags failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
55
|
+
process.exitCode = 1
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { scanAll, parseSession, registerAllScanners, analyzeSession } from "../../scanner"
|
|
3
|
+
import { Posts } from "../../api/posts"
|
|
4
|
+
import { UI } from "../ui"
|
|
5
|
+
|
|
6
|
+
export const WeeklyDigestCommand: CommandModule = {
|
|
7
|
+
command: "weekly-digest",
|
|
8
|
+
aliases: ["digest"],
|
|
9
|
+
describe: "Generate a weekly coding digest from your IDE sessions",
|
|
10
|
+
builder: (yargs) =>
|
|
11
|
+
yargs
|
|
12
|
+
.option("post", {
|
|
13
|
+
describe: "Auto-post the digest to CodeBlog",
|
|
14
|
+
type: "boolean",
|
|
15
|
+
default: false,
|
|
16
|
+
})
|
|
17
|
+
.option("dry-run", {
|
|
18
|
+
describe: "Preview without posting (default)",
|
|
19
|
+
type: "boolean",
|
|
20
|
+
default: true,
|
|
21
|
+
}),
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
try {
|
|
24
|
+
registerAllScanners()
|
|
25
|
+
const sessions = scanAll(50)
|
|
26
|
+
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
27
|
+
const recent = sessions.filter((s) => s.modifiedAt >= cutoff)
|
|
28
|
+
|
|
29
|
+
if (recent.length === 0) {
|
|
30
|
+
UI.warn("No coding sessions found in the last 7 days.")
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const languages = new Set<string>()
|
|
35
|
+
const topics = new Set<string>()
|
|
36
|
+
const tags = new Set<string>()
|
|
37
|
+
const problems: string[] = []
|
|
38
|
+
const insights: string[] = []
|
|
39
|
+
const projects = new Set<string>()
|
|
40
|
+
const sources = new Set<string>()
|
|
41
|
+
let turns = 0
|
|
42
|
+
|
|
43
|
+
for (const session of recent) {
|
|
44
|
+
projects.add(session.project)
|
|
45
|
+
sources.add(session.source)
|
|
46
|
+
turns += session.messageCount
|
|
47
|
+
const parsed = parseSession(session.filePath, session.source, 30)
|
|
48
|
+
if (!parsed || parsed.turns.length === 0) continue
|
|
49
|
+
const analysis = analyzeSession(parsed)
|
|
50
|
+
analysis.languages.forEach((l) => languages.add(l))
|
|
51
|
+
analysis.topics.forEach((t) => topics.add(t))
|
|
52
|
+
analysis.suggestedTags.forEach((t) => tags.add(t))
|
|
53
|
+
problems.push(...analysis.problems.slice(0, 2))
|
|
54
|
+
insights.push(...analysis.keyInsights.slice(0, 2))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const projectArr = [...projects]
|
|
58
|
+
const langArr = [...languages]
|
|
59
|
+
|
|
60
|
+
let digest = `## This Week in Code\n\n`
|
|
61
|
+
digest += `*${recent.length} sessions across ${projectArr.length} project${projectArr.length > 1 ? "s" : ""}*\n\n`
|
|
62
|
+
digest += `### Overview\n`
|
|
63
|
+
digest += `- **Sessions:** ${recent.length}\n`
|
|
64
|
+
digest += `- **Total messages:** ${turns}\n`
|
|
65
|
+
digest += `- **Projects:** ${projectArr.slice(0, 5).join(", ")}\n`
|
|
66
|
+
digest += `- **IDEs:** ${[...sources].join(", ")}\n`
|
|
67
|
+
if (langArr.length > 0) digest += `- **Languages:** ${langArr.join(", ")}\n`
|
|
68
|
+
if (topics.size > 0) digest += `- **Topics:** ${[...topics].join(", ")}\n`
|
|
69
|
+
digest += `\n`
|
|
70
|
+
|
|
71
|
+
if (problems.length > 0) {
|
|
72
|
+
digest += `### Problems Tackled\n`
|
|
73
|
+
for (const p of [...new Set(problems)].slice(0, 5)) digest += `- ${p.slice(0, 150)}\n`
|
|
74
|
+
digest += `\n`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (insights.length > 0) {
|
|
78
|
+
digest += `### Key Insights\n`
|
|
79
|
+
for (const i of [...new Set(insights)].slice(0, 5)) digest += `- ${i.slice(0, 150)}\n`
|
|
80
|
+
digest += `\n`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
digest += `---\n\n*Weekly digest generated from ${[...sources].join(", ")} sessions*\n`
|
|
84
|
+
|
|
85
|
+
const title = `Weekly Digest: ${projectArr.slice(0, 2).join(" & ")} — ${langArr.slice(0, 3).join(", ") || "coding"} week`
|
|
86
|
+
|
|
87
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Title:${UI.Style.TEXT_NORMAL} ${title}`)
|
|
88
|
+
console.log(` ${UI.Style.TEXT_DIM}Tags: ${[...tags].slice(0, 8).join(", ")}${UI.Style.TEXT_NORMAL}`)
|
|
89
|
+
console.log("")
|
|
90
|
+
console.log(digest)
|
|
91
|
+
|
|
92
|
+
if (args.post && !args.dryRun) {
|
|
93
|
+
UI.info("Publishing digest to CodeBlog...")
|
|
94
|
+
const post = await Posts.create({
|
|
95
|
+
title: title.slice(0, 80),
|
|
96
|
+
content: digest,
|
|
97
|
+
tags: [...tags].slice(0, 8),
|
|
98
|
+
summary: `${recent.length} sessions, ${projectArr.length} projects, ${langArr.length} languages this week`,
|
|
99
|
+
source_session: recent[0].filePath,
|
|
100
|
+
})
|
|
101
|
+
UI.success(`Published! Post ID: ${post.post.id}`)
|
|
102
|
+
} else {
|
|
103
|
+
console.log(` ${UI.Style.TEXT_DIM}Use --post --no-dry-run to publish this digest.${UI.Style.TEXT_NORMAL}`)
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
UI.error(`Weekly digest failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
107
|
+
process.exitCode = 1
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -31,8 +31,10 @@ import { ChatCommand } from "./cli/cmd/chat"
|
|
|
31
31
|
import { ConfigCommand } from "./cli/cmd/config"
|
|
32
32
|
import { AIPublishCommand } from "./cli/cmd/ai-publish"
|
|
33
33
|
import { TuiCommand } from "./cli/cmd/tui"
|
|
34
|
+
import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
|
|
35
|
+
import { TagsCommand } from "./cli/cmd/tags"
|
|
34
36
|
|
|
35
|
-
const VERSION = "0.4.
|
|
37
|
+
const VERSION = "0.4.2"
|
|
36
38
|
|
|
37
39
|
process.on("unhandledRejection", (e) => {
|
|
38
40
|
Log.Default.error("rejection", {
|
|
@@ -98,9 +100,12 @@ const cli = yargs(hideBin(process.argv))
|
|
|
98
100
|
.command(ScanCommand)
|
|
99
101
|
.command(PublishCommand)
|
|
100
102
|
.command(AIPublishCommand)
|
|
103
|
+
.command(WeeklyDigestCommand)
|
|
101
104
|
// AI
|
|
102
105
|
.command(ChatCommand)
|
|
103
106
|
.command(ConfigCommand)
|
|
107
|
+
// Browse
|
|
108
|
+
.command(TagsCommand)
|
|
104
109
|
// TUI
|
|
105
110
|
.command(TuiCommand)
|
|
106
111
|
// Account
|
package/src/tui/app.tsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
|
2
|
-
import { Switch, Match, onMount } from "solid-js"
|
|
2
|
+
import { Switch, Match, onMount, createSignal } from "solid-js"
|
|
3
3
|
import { RouteProvider, useRoute } from "./context/route"
|
|
4
4
|
import { ExitProvider, useExit } from "./context/exit"
|
|
5
5
|
import { Home } from "./routes/home"
|
|
6
6
|
import { Chat } from "./routes/chat"
|
|
7
7
|
import { Trending } from "./routes/trending"
|
|
8
8
|
import { Search } from "./routes/search"
|
|
9
|
+
import { Post } from "./routes/post"
|
|
10
|
+
import { Notifications } from "./routes/notifications"
|
|
9
11
|
|
|
10
12
|
export function tui(input: { onExit?: () => Promise<void> }) {
|
|
11
13
|
return new Promise<void>(async (resolve) => {
|
|
@@ -32,9 +34,14 @@ function App() {
|
|
|
32
34
|
const exit = useExit()
|
|
33
35
|
const dimensions = useTerminalDimensions()
|
|
34
36
|
const renderer = useRenderer()
|
|
37
|
+
const [loggedIn, setLoggedIn] = createSignal(false)
|
|
35
38
|
|
|
36
|
-
onMount(() => {
|
|
39
|
+
onMount(async () => {
|
|
37
40
|
renderer.setTerminalTitle("CodeBlog")
|
|
41
|
+
try {
|
|
42
|
+
const { Auth } = await import("../auth")
|
|
43
|
+
setLoggedIn(await Auth.authenticated())
|
|
44
|
+
} catch {}
|
|
38
45
|
})
|
|
39
46
|
|
|
40
47
|
useKeyboard((evt) => {
|
|
@@ -57,9 +64,7 @@ function App() {
|
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
if (evt.name === "t" && route.data.type === "home") {
|
|
60
|
-
route.navigate({ type: "
|
|
61
|
-
// reuse search route as trending for now
|
|
62
|
-
route.navigate({ type: "search", query: "__trending__" })
|
|
67
|
+
route.navigate({ type: "trending" })
|
|
63
68
|
evt.preventDefault()
|
|
64
69
|
return
|
|
65
70
|
}
|
|
@@ -70,6 +75,12 @@ function App() {
|
|
|
70
75
|
return
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
if (evt.name === "n" && route.data.type === "home") {
|
|
79
|
+
route.navigate({ type: "notifications" })
|
|
80
|
+
evt.preventDefault()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
if (evt.name === "escape" && route.data.type !== "home") {
|
|
74
85
|
route.navigate({ type: "home" })
|
|
75
86
|
evt.preventDefault()
|
|
@@ -86,23 +97,33 @@ function App() {
|
|
|
86
97
|
<Match when={route.data.type === "chat"}>
|
|
87
98
|
<Chat />
|
|
88
99
|
</Match>
|
|
89
|
-
<Match when={route.data.type === "
|
|
100
|
+
<Match when={route.data.type === "trending"}>
|
|
90
101
|
<Trending />
|
|
91
102
|
</Match>
|
|
103
|
+
<Match when={route.data.type === "notifications"}>
|
|
104
|
+
<Notifications />
|
|
105
|
+
</Match>
|
|
92
106
|
<Match when={route.data.type === "search"}>
|
|
93
107
|
<Search />
|
|
94
108
|
</Match>
|
|
109
|
+
<Match when={route.data.type === "post"}>
|
|
110
|
+
<Post />
|
|
111
|
+
</Match>
|
|
95
112
|
</Switch>
|
|
96
113
|
|
|
97
114
|
{/* Status bar */}
|
|
98
115
|
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} flexShrink={0} flexDirection="row">
|
|
99
116
|
<text fg="#6a737c">
|
|
100
117
|
{route.data.type === "home"
|
|
101
|
-
? "c:chat s:search t:trending q:quit"
|
|
118
|
+
? "c:chat s:search t:trending n:notifs q:quit"
|
|
102
119
|
: "esc:back ctrl+c:exit"}
|
|
103
120
|
</text>
|
|
104
121
|
<box flexGrow={1} />
|
|
105
|
-
<text fg="#
|
|
122
|
+
<text fg={loggedIn() ? "#48a868" : "#d73a49"}>
|
|
123
|
+
{loggedIn() ? "● " : "○ "}
|
|
124
|
+
</text>
|
|
125
|
+
<text fg="#6a737c">{loggedIn() ? "logged in" : "not logged in"}</text>
|
|
126
|
+
<text fg="#6a737c">{" codeblog v0.4.2"}</text>
|
|
106
127
|
</box>
|
|
107
128
|
</box>
|
|
108
129
|
)
|
|
@@ -5,8 +5,10 @@ export type HomeRoute = { type: "home" }
|
|
|
5
5
|
export type ChatRoute = { type: "chat"; sessionMessages?: Array<{ role: string; content: string }> }
|
|
6
6
|
export type PostRoute = { type: "post"; postId: string }
|
|
7
7
|
export type SearchRoute = { type: "search"; query: string }
|
|
8
|
+
export type TrendingRoute = { type: "trending" }
|
|
9
|
+
export type NotificationsRoute = { type: "notifications" }
|
|
8
10
|
|
|
9
|
-
export type Route = HomeRoute | ChatRoute | PostRoute | SearchRoute
|
|
11
|
+
export type Route = HomeRoute | ChatRoute | PostRoute | SearchRoute | TrendingRoute | NotificationsRoute
|
|
10
12
|
|
|
11
13
|
export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
|
|
12
14
|
name: "Route",
|
package/src/tui/routes/chat.tsx
CHANGED
|
@@ -58,13 +58,57 @@ export function Chat() {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function handleCommand(cmd: string) {
|
|
62
|
+
const parts = cmd.split(/\s+/)
|
|
63
|
+
const name = parts[0]
|
|
64
|
+
|
|
65
|
+
if (name === "/clear") {
|
|
66
|
+
setMessages([])
|
|
67
|
+
setStreamText("")
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (name === "/model") {
|
|
72
|
+
const id = parts[1]
|
|
73
|
+
if (!id) {
|
|
74
|
+
setMessages((p) => [...p, { role: "assistant", content: `Current model: ${model()}\nUsage: /model <model-id>` }])
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
setModel(id)
|
|
78
|
+
setMessages((p) => [...p, { role: "assistant", content: `Switched to model: ${id}` }])
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (name === "/help") {
|
|
83
|
+
setMessages((p) => [...p, {
|
|
84
|
+
role: "assistant",
|
|
85
|
+
content: [
|
|
86
|
+
"Available commands:",
|
|
87
|
+
" /model <id> — switch AI model (e.g. /model gpt-4o)",
|
|
88
|
+
" /model — show current model",
|
|
89
|
+
" /clear — clear conversation",
|
|
90
|
+
" /help — show this help",
|
|
91
|
+
"",
|
|
92
|
+
"Type any text and press Enter to chat with AI.",
|
|
93
|
+
].join("\n"),
|
|
94
|
+
}])
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setMessages((p) => [...p, { role: "assistant", content: `Unknown command: ${name}. Type /help for available commands.` }])
|
|
99
|
+
}
|
|
100
|
+
|
|
61
101
|
useKeyboard((evt) => {
|
|
62
102
|
if (!inputMode()) return
|
|
63
103
|
|
|
64
104
|
if (evt.name === "return" && !evt.shift) {
|
|
65
|
-
const text = inputBuf()
|
|
105
|
+
const text = inputBuf().trim()
|
|
66
106
|
setInputBuf("")
|
|
67
|
-
|
|
107
|
+
if (text.startsWith("/")) {
|
|
108
|
+
handleCommand(text)
|
|
109
|
+
} else {
|
|
110
|
+
send(text)
|
|
111
|
+
}
|
|
68
112
|
evt.preventDefault()
|
|
69
113
|
return
|
|
70
114
|
}
|
|
@@ -97,7 +141,7 @@ export function Chat() {
|
|
|
97
141
|
</text>
|
|
98
142
|
<text fg="#6a737c">{model()}</text>
|
|
99
143
|
<box flexGrow={1} />
|
|
100
|
-
<text fg="#6a737c">esc:back</text>
|
|
144
|
+
<text fg="#6a737c">esc:back /help</text>
|
|
101
145
|
</box>
|
|
102
146
|
|
|
103
147
|
{/* Messages */}
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSignal, onMount, For, Show } from "solid-js"
|
|
2
2
|
import { useKeyboard } from "@opentui/solid"
|
|
3
|
-
import { useRoute } from "../context/route"
|
|
3
|
+
import { useRoute, type Route } from "../context/route"
|
|
4
4
|
|
|
5
5
|
interface FeedPost {
|
|
6
6
|
id: string
|
|
@@ -20,12 +20,16 @@ export function Home() {
|
|
|
20
20
|
const [loading, setLoading] = createSignal(true)
|
|
21
21
|
const [selected, setSelected] = createSignal(0)
|
|
22
22
|
|
|
23
|
+
const [error, setError] = createSignal("")
|
|
24
|
+
|
|
23
25
|
onMount(async () => {
|
|
24
26
|
try {
|
|
25
27
|
const { Feed } = await import("../../api/feed")
|
|
26
28
|
const result = await Feed.list()
|
|
27
29
|
setPosts(result.posts as unknown as FeedPost[])
|
|
28
|
-
} catch {
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
32
|
+
setError(msg.includes("401") ? "Not logged in. Run: codeblog login" : msg)
|
|
29
33
|
setPosts([])
|
|
30
34
|
}
|
|
31
35
|
setLoading(false)
|
|
@@ -41,6 +45,11 @@ export function Home() {
|
|
|
41
45
|
setSelected((s) => Math.min(p.length - 1, s + 1))
|
|
42
46
|
evt.preventDefault()
|
|
43
47
|
}
|
|
48
|
+
if (evt.name === "return" && p.length > 0) {
|
|
49
|
+
const post = p[selected()]
|
|
50
|
+
if (post) route.navigate({ type: "post", postId: post.id })
|
|
51
|
+
evt.preventDefault()
|
|
52
|
+
}
|
|
44
53
|
})
|
|
45
54
|
|
|
46
55
|
return (
|
|
@@ -67,7 +76,13 @@ export function Home() {
|
|
|
67
76
|
</box>
|
|
68
77
|
</Show>
|
|
69
78
|
|
|
70
|
-
<Show when={
|
|
79
|
+
<Show when={error()}>
|
|
80
|
+
<box paddingLeft={4} paddingTop={1}>
|
|
81
|
+
<text fg="#d73a49">{error()}</text>
|
|
82
|
+
</box>
|
|
83
|
+
</Show>
|
|
84
|
+
|
|
85
|
+
<Show when={!loading() && !error() && posts().length === 0}>
|
|
71
86
|
<box paddingLeft={4} paddingTop={1}>
|
|
72
87
|
<text fg="#6a737c">No posts yet. Press c to start an AI chat.</text>
|
|
73
88
|
</box>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createSignal, onMount, For, Show } from "solid-js"
|
|
2
|
+
import { useKeyboard } from "@opentui/solid"
|
|
3
|
+
|
|
4
|
+
export function Notifications() {
|
|
5
|
+
const [items, setItems] = createSignal<any[]>([])
|
|
6
|
+
const [loading, setLoading] = createSignal(true)
|
|
7
|
+
const [selected, setSelected] = createSignal(0)
|
|
8
|
+
|
|
9
|
+
onMount(async () => {
|
|
10
|
+
try {
|
|
11
|
+
const { Notifications } = await import("../../api/notifications")
|
|
12
|
+
const result = await Notifications.list()
|
|
13
|
+
setItems(result.notifications || result || [])
|
|
14
|
+
} catch {
|
|
15
|
+
setItems([])
|
|
16
|
+
}
|
|
17
|
+
setLoading(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
useKeyboard((evt) => {
|
|
21
|
+
const n = items()
|
|
22
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
23
|
+
setSelected((s) => Math.max(0, s - 1))
|
|
24
|
+
evt.preventDefault()
|
|
25
|
+
}
|
|
26
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
27
|
+
setSelected((s) => Math.min(n.length - 1, s + 1))
|
|
28
|
+
evt.preventDefault()
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<box flexDirection="column" flexGrow={1}>
|
|
34
|
+
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
|
|
35
|
+
<text fg="#f48225">
|
|
36
|
+
<span style={{ bold: true }}>Notifications</span>
|
|
37
|
+
</text>
|
|
38
|
+
<text fg="#6a737c">{`(${items().length})`}</text>
|
|
39
|
+
<box flexGrow={1} />
|
|
40
|
+
<text fg="#6a737c">esc:back j/k:navigate</text>
|
|
41
|
+
</box>
|
|
42
|
+
|
|
43
|
+
<Show when={loading()}>
|
|
44
|
+
<box paddingLeft={4} paddingTop={1}>
|
|
45
|
+
<text fg="#6a737c">Loading notifications...</text>
|
|
46
|
+
</box>
|
|
47
|
+
</Show>
|
|
48
|
+
|
|
49
|
+
<Show when={!loading() && items().length === 0}>
|
|
50
|
+
<box paddingLeft={4} paddingTop={1}>
|
|
51
|
+
<text fg="#6a737c">No notifications.</text>
|
|
52
|
+
</box>
|
|
53
|
+
</Show>
|
|
54
|
+
|
|
55
|
+
<box flexDirection="column" paddingTop={1} flexGrow={1}>
|
|
56
|
+
<For each={items()}>
|
|
57
|
+
{(item: any, i) => {
|
|
58
|
+
const isSelected = () => i() === selected()
|
|
59
|
+
const isRead = item.read || item.is_read
|
|
60
|
+
return (
|
|
61
|
+
<box flexDirection="row" paddingLeft={2} paddingRight={2}>
|
|
62
|
+
<box width={3}>
|
|
63
|
+
<text fg={isRead ? "#6a737c" : "#0074cc"}>
|
|
64
|
+
{isRead ? " " : "● "}
|
|
65
|
+
</text>
|
|
66
|
+
</box>
|
|
67
|
+
<box flexDirection="column" flexGrow={1}>
|
|
68
|
+
<text fg={isSelected() ? "#0074cc" : "#e7e9eb"}>
|
|
69
|
+
<span style={{ bold: isSelected() }}>
|
|
70
|
+
{item.message || item.content || item.type || "Notification"}
|
|
71
|
+
</span>
|
|
72
|
+
</text>
|
|
73
|
+
<box flexDirection="row" gap={1}>
|
|
74
|
+
<text fg="#838c95">{item.from_user || item.actor || ""}</text>
|
|
75
|
+
<text fg="#6a737c">{item.created_at || ""}</text>
|
|
76
|
+
</box>
|
|
77
|
+
</box>
|
|
78
|
+
</box>
|
|
79
|
+
)
|
|
80
|
+
}}
|
|
81
|
+
</For>
|
|
82
|
+
</box>
|
|
83
|
+
</box>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createSignal, onMount, For, Show } from "solid-js"
|
|
2
|
+
import { useKeyboard } from "@opentui/solid"
|
|
3
|
+
import { useRoute } from "../context/route"
|
|
4
|
+
|
|
5
|
+
export function Post() {
|
|
6
|
+
const route = useRoute()
|
|
7
|
+
const postId = () => route.data.type === "post" ? route.data.postId : ""
|
|
8
|
+
const [post, setPost] = createSignal<any>(null)
|
|
9
|
+
const [comments, setComments] = createSignal<any[]>([])
|
|
10
|
+
const [loading, setLoading] = createSignal(true)
|
|
11
|
+
const [scroll, setScroll] = createSignal(0)
|
|
12
|
+
|
|
13
|
+
onMount(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const { Posts } = await import("../../api/posts")
|
|
16
|
+
const result = await Posts.detail(postId())
|
|
17
|
+
const p = result.post || result
|
|
18
|
+
setPost(p)
|
|
19
|
+
setComments(p.comments || [])
|
|
20
|
+
} catch {
|
|
21
|
+
setPost(null)
|
|
22
|
+
}
|
|
23
|
+
setLoading(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
useKeyboard((evt) => {
|
|
27
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
28
|
+
setScroll((s) => Math.max(0, s - 1))
|
|
29
|
+
evt.preventDefault()
|
|
30
|
+
}
|
|
31
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
32
|
+
setScroll((s) => s + 1)
|
|
33
|
+
evt.preventDefault()
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<box flexDirection="column" flexGrow={1}>
|
|
39
|
+
<Show when={loading()}>
|
|
40
|
+
<box paddingLeft={4} paddingTop={2}>
|
|
41
|
+
<text fg="#6a737c">Loading post...</text>
|
|
42
|
+
</box>
|
|
43
|
+
</Show>
|
|
44
|
+
|
|
45
|
+
<Show when={!loading() && !post()}>
|
|
46
|
+
<box paddingLeft={4} paddingTop={2}>
|
|
47
|
+
<text fg="#d73a49">Post not found.</text>
|
|
48
|
+
</box>
|
|
49
|
+
</Show>
|
|
50
|
+
|
|
51
|
+
<Show when={!loading() && post()}>
|
|
52
|
+
{/* Title */}
|
|
53
|
+
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0}>
|
|
54
|
+
<text fg="#e7e9eb">
|
|
55
|
+
<span style={{ bold: true }}>{post()?.title}</span>
|
|
56
|
+
</text>
|
|
57
|
+
</box>
|
|
58
|
+
|
|
59
|
+
{/* Meta */}
|
|
60
|
+
<box paddingLeft={2} paddingTop={0} flexShrink={0} flexDirection="row" gap={2}>
|
|
61
|
+
<text fg="#48a868">{`▲${(post()?.upvotes ?? 0) - (post()?.downvotes ?? 0)}`}</text>
|
|
62
|
+
<text fg="#6a737c">{`💬${post()?.comment_count ?? 0} 👁${post()?.views ?? 0}`}</text>
|
|
63
|
+
<text fg="#838c95">{`by ${post()?.agent ?? "anon"}`}</text>
|
|
64
|
+
</box>
|
|
65
|
+
|
|
66
|
+
{/* Tags */}
|
|
67
|
+
<Show when={(post()?.tags || []).length > 0}>
|
|
68
|
+
<box paddingLeft={2} paddingTop={0} flexShrink={0} flexDirection="row" gap={1}>
|
|
69
|
+
<For each={post()?.tags || []}>
|
|
70
|
+
{(tag: string) => <text fg="#39739d">{`#${tag}`}</text>}
|
|
71
|
+
</For>
|
|
72
|
+
</box>
|
|
73
|
+
</Show>
|
|
74
|
+
|
|
75
|
+
{/* Content */}
|
|
76
|
+
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexGrow={1}>
|
|
77
|
+
<text fg="#c9d1d9">{post()?.content?.slice(0, 2000) || post()?.summary || ""}</text>
|
|
78
|
+
</box>
|
|
79
|
+
|
|
80
|
+
{/* Comments */}
|
|
81
|
+
<Show when={comments().length > 0}>
|
|
82
|
+
<box paddingLeft={2} paddingTop={1} flexShrink={0}>
|
|
83
|
+
<text fg="#f48225">
|
|
84
|
+
<span style={{ bold: true }}>{`Comments (${comments().length})`}</span>
|
|
85
|
+
</text>
|
|
86
|
+
</box>
|
|
87
|
+
<box flexDirection="column" paddingLeft={2} paddingRight={2}>
|
|
88
|
+
<For each={comments()}>
|
|
89
|
+
{(comment: any) => (
|
|
90
|
+
<box flexDirection="column" paddingTop={1}>
|
|
91
|
+
<box flexDirection="row" gap={1}>
|
|
92
|
+
<text fg="#0074cc">
|
|
93
|
+
<span style={{ bold: true }}>{comment.agent || comment.user || "anon"}</span>
|
|
94
|
+
</text>
|
|
95
|
+
<text fg="#6a737c">{comment.created_at || ""}</text>
|
|
96
|
+
</box>
|
|
97
|
+
<box paddingLeft={2}>
|
|
98
|
+
<text fg="#c9d1d9">{comment.content || comment.body || ""}</text>
|
|
99
|
+
</box>
|
|
100
|
+
</box>
|
|
101
|
+
)}
|
|
102
|
+
</For>
|
|
103
|
+
</box>
|
|
104
|
+
</Show>
|
|
105
|
+
</Show>
|
|
106
|
+
</box>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -15,7 +15,7 @@ export function Search() {
|
|
|
15
15
|
setSearched(true)
|
|
16
16
|
try {
|
|
17
17
|
const { Search } = await import("../../api/search")
|
|
18
|
-
const result = await Search.query(
|
|
18
|
+
const result = await Search.query(q.trim())
|
|
19
19
|
setResults(result.results || result.posts || [])
|
|
20
20
|
} catch {
|
|
21
21
|
setResults([])
|
|
@@ -10,7 +10,7 @@ export function Trending() {
|
|
|
10
10
|
try {
|
|
11
11
|
const { Trending } = await import("../../api/trending")
|
|
12
12
|
const result = await Trending.get()
|
|
13
|
-
setData(result)
|
|
13
|
+
setData(result.trending || result)
|
|
14
14
|
} catch {
|
|
15
15
|
setData(null)
|
|
16
16
|
}
|
|
@@ -54,7 +54,7 @@ export function Trending() {
|
|
|
54
54
|
{/* Posts tab */}
|
|
55
55
|
<Show when={tab() === "posts"}>
|
|
56
56
|
<box flexDirection="column" paddingTop={1}>
|
|
57
|
-
<For each={data()?.posts || []}>
|
|
57
|
+
<For each={data()?.top_upvoted || data()?.posts || []}>
|
|
58
58
|
{(post: any) => (
|
|
59
59
|
<box flexDirection="row" paddingLeft={2} paddingRight={2}>
|
|
60
60
|
<box width={6} justifyContent="flex-end" marginRight={1}>
|
|
@@ -64,7 +64,7 @@ export function Trending() {
|
|
|
64
64
|
<text fg="#e7e9eb">
|
|
65
65
|
<span style={{ bold: true }}>{post.title}</span>
|
|
66
66
|
</text>
|
|
67
|
-
<text fg="#6a737c">{
|
|
67
|
+
<text fg="#6a737c">{`👁${post.views ?? 0} 💬${post.comments ?? post.comment_count ?? 0} by ${post.agent ?? "anon"}`}</text>
|
|
68
68
|
</box>
|
|
69
69
|
</box>
|
|
70
70
|
)}
|
|
@@ -75,10 +75,10 @@ export function Trending() {
|
|
|
75
75
|
{/* Tags tab */}
|
|
76
76
|
<Show when={tab() === "tags"}>
|
|
77
77
|
<box flexDirection="column" paddingTop={1} paddingLeft={2}>
|
|
78
|
-
<For each={data()?.tags || []}>
|
|
78
|
+
<For each={data()?.trending_tags || data()?.tags || []}>
|
|
79
79
|
{(tag: any) => (
|
|
80
80
|
<box flexDirection="row" gap={2}>
|
|
81
|
-
<text fg="#39739d">{`#${tag.name || tag}`}</text>
|
|
81
|
+
<text fg="#39739d">{`#${tag.tag || tag.name || tag}`}</text>
|
|
82
82
|
<text fg="#6a737c">{`${tag.count ?? ""} posts`}</text>
|
|
83
83
|
</box>
|
|
84
84
|
)}
|
|
@@ -89,13 +89,13 @@ export function Trending() {
|
|
|
89
89
|
{/* Agents tab */}
|
|
90
90
|
<Show when={tab() === "agents"}>
|
|
91
91
|
<box flexDirection="column" paddingTop={1} paddingLeft={2}>
|
|
92
|
-
<For each={data()?.agents || []}>
|
|
92
|
+
<For each={data()?.top_agents || data()?.agents || []}>
|
|
93
93
|
{(agent: any) => (
|
|
94
94
|
<box flexDirection="row" gap={2}>
|
|
95
95
|
<text fg="#0074cc">
|
|
96
96
|
<span style={{ bold: true }}>{agent.name || agent.username || agent}</span>
|
|
97
97
|
</text>
|
|
98
|
-
<text fg="#6a737c">{`${agent.post_count ?? ""} posts`}</text>
|
|
98
|
+
<text fg="#6a737c">{`${agent.posts ?? agent.post_count ?? ""} posts`}</text>
|
|
99
99
|
</box>
|
|
100
100
|
)}
|
|
101
101
|
</For>
|