create-manifest 1.3.3 → 2.0.0

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.
Files changed (57) hide show
  1. package/README.md +39 -24
  2. package/index.js +51 -0
  3. package/package.json +11 -89
  4. package/starter/.claude/settings.local.json +21 -0
  5. package/starter/.env.example +1 -0
  6. package/starter/@/components/table.tsx +478 -0
  7. package/starter/@/components/ui/button.tsx +62 -0
  8. package/starter/@/components/ui/checkbox.tsx +30 -0
  9. package/starter/README-DEV.md +167 -0
  10. package/starter/components.json +24 -0
  11. package/starter/package.json +42 -0
  12. package/starter/src/flows/list-pokemons.flow.ts +131 -0
  13. package/starter/src/server.ts +165 -0
  14. package/starter/src/web/PokemonList.tsx +125 -0
  15. package/starter/src/web/components/blog-post-card.tsx +286 -0
  16. package/starter/src/web/components/blog-post-list.tsx +291 -0
  17. package/starter/src/web/components/ui/.gitkeep +0 -0
  18. package/starter/src/web/components/ui/button.tsx +62 -0
  19. package/starter/src/web/globals.css +98 -0
  20. package/starter/src/web/hooks/.gitkeep +0 -0
  21. package/starter/src/web/lib/utils.ts +6 -0
  22. package/starter/src/web/root.tsx +36 -0
  23. package/starter/src/web/tsconfig.json +3 -0
  24. package/starter/tsconfig.json +21 -0
  25. package/starter/tsconfig.web.json +24 -0
  26. package/starter/vite.config.ts +36 -0
  27. package/assets/monorepo/README.md +0 -51
  28. package/assets/monorepo/api-package.json +0 -9
  29. package/assets/monorepo/api-readme.md +0 -49
  30. package/assets/monorepo/manifest.yml +0 -34
  31. package/assets/monorepo/root-package.json +0 -15
  32. package/assets/monorepo/web-package.json +0 -10
  33. package/assets/monorepo/web-readme.md +0 -9
  34. package/assets/standalone/README.md +0 -49
  35. package/assets/standalone/api-package.json +0 -9
  36. package/assets/standalone/manifest.yml +0 -34
  37. package/bin/dev.cmd +0 -3
  38. package/bin/dev.js +0 -5
  39. package/bin/run.cmd +0 -3
  40. package/bin/run.js +0 -5
  41. package/dist/commands/index.d.ts +0 -65
  42. package/dist/commands/index.js +0 -480
  43. package/dist/index.d.ts +0 -1
  44. package/dist/index.js +0 -1
  45. package/dist/utils/GetBackendFileContent.d.ts +0 -1
  46. package/dist/utils/GetBackendFileContent.js +0 -21
  47. package/dist/utils/GetLatestPackageVersion.d.ts +0 -1
  48. package/dist/utils/GetLatestPackageVersion.js +0 -5
  49. package/dist/utils/UpdateExtensionJsonFile.d.ts +0 -6
  50. package/dist/utils/UpdateExtensionJsonFile.js +0 -8
  51. package/dist/utils/UpdatePackageJsonFile.d.ts +0 -18
  52. package/dist/utils/UpdatePackageJsonFile.js +0 -21
  53. package/dist/utils/UpdateSettingsJsonFile.d.ts +0 -4
  54. package/dist/utils/UpdateSettingsJsonFile.js +0 -6
  55. package/dist/utils/helpers.d.ts +0 -1
  56. package/dist/utils/helpers.js +0 -11
  57. package/oclif.manifest.json +0 -47
@@ -0,0 +1,167 @@
1
+ # Manifest MCP Server - Developer Guide
2
+
3
+ This is an MCP (Model Context Protocol) server template for building AI-powered flows.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ src/
9
+ ├── server.ts # Express + MCP server setup
10
+ ├── flows/ # MCP flows (tools, resources)
11
+ │ └── gameboy.flow.ts # Example flow
12
+ └── web/ # Web components (UI widgets)
13
+ └── gameboy-player/ # Example web component
14
+ ├── gameboy-player.html
15
+ ├── gameboy-player.ts
16
+ └── gameboy-player.css
17
+ scripts/
18
+ └── build-web.ts # Vite build script for web components
19
+ ```
20
+
21
+ ## Getting Started
22
+
23
+ ```bash
24
+ # Install dependencies
25
+ npm install
26
+
27
+ # Start development server (with hot reload)
28
+ npm run dev
29
+
30
+ # Build for production
31
+ npm run build
32
+
33
+ # Run production build
34
+ npm start
35
+ ```
36
+
37
+ ## Environment Variables
38
+
39
+ Copy `.env.example` to `.env` and configure:
40
+
41
+ | Variable | Default | Description |
42
+ |----------|---------|-------------|
43
+ | `PORT` | `3000` | Server port |
44
+
45
+ ## Creating Flows
46
+
47
+ Flows register tools and resources with the MCP server. See `src/flows/gameboy.flow.ts` for a complete example.
48
+
49
+ ### Basic Flow Structure
50
+
51
+ ```typescript
52
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
53
+ import { z } from "zod"
54
+
55
+ export function registerMyFlow(server: McpServer): void {
56
+ // Register a tool
57
+ server.registerTool(
58
+ "myTool",
59
+ {
60
+ title: "My Tool",
61
+ description: "Does something useful",
62
+ inputSchema: z.object({
63
+ input: z.string().describe("The input parameter")
64
+ })
65
+ },
66
+ async (args) => {
67
+ return {
68
+ content: [{ type: "text", text: `Result: ${args.input}` }]
69
+ }
70
+ }
71
+ )
72
+
73
+ // Register a resource (UI widget)
74
+ server.registerResource("my-widget", "ui://my-widget.html", {}, async () => ({
75
+ contents: [
76
+ {
77
+ uri: "ui://my-widget.html",
78
+ mimeType: "text/html+skybridge",
79
+ text: "<html>...</html>"
80
+ }
81
+ ]
82
+ }))
83
+ }
84
+ ```
85
+
86
+ ### Registering Your Flow
87
+
88
+ Add your flow to `src/server.ts`:
89
+
90
+ ```typescript
91
+ import { registerMyFlow } from "./flows/my.flow.js"
92
+
93
+ function createServer() {
94
+ const server = new McpServer({
95
+ name: "My MCP Server",
96
+ version: "0.0.1"
97
+ })
98
+
99
+ registerMyFlow(server)
100
+
101
+ return server
102
+ }
103
+ ```
104
+
105
+ ## Creating Web Components
106
+
107
+ Web components are built with Vite and bundled into single HTML files.
108
+
109
+ 1. Create a folder in `src/web/` with your component files
110
+ 2. Add the component to `scripts/build-web.ts`
111
+ 3. Reference the built HTML in your flow
112
+
113
+ ### Build Configuration
114
+
115
+ Edit `scripts/build-web.ts` to add new web components:
116
+
117
+ ```typescript
118
+ const components = [
119
+ 'gameboy-player',
120
+ 'my-new-component' // Add your component here
121
+ ]
122
+ ```
123
+
124
+ ## MCP Endpoints
125
+
126
+ The server exposes the following endpoints:
127
+
128
+ | Method | Endpoint | Description |
129
+ |--------|----------|-------------|
130
+ | `POST` | `/mcp` | Create session / Send messages |
131
+ | `GET` | `/mcp` | SSE stream for server events |
132
+ | `DELETE` | `/mcp` | Close session |
133
+
134
+ Sessions are managed via the `mcp-session-id` header.
135
+
136
+ ## Development Tips
137
+
138
+ - The server uses Nodemon for hot reload during development
139
+ - Web components are rebuilt on file changes
140
+ - Use `console.log()` for debugging - output appears in the terminal
141
+
142
+ ## Testing with ChatGPT
143
+
144
+ 1. Deploy your server to a public URL (or use ngrok for local testing)
145
+ 2. Add the MCP server URL in ChatGPT settings
146
+ 3. Your tools will be available in the chat
147
+
148
+ ## Available Scripts
149
+
150
+ | Script | Description |
151
+ |--------|-------------|
152
+ | `npm run dev` | Start dev server with hot reload |
153
+ | `npm run build` | Build TypeScript and web components |
154
+ | `npm run build:web` | Build only web components |
155
+ | `npm start` | Run production build |
156
+
157
+ ## Dependencies
158
+
159
+ - `@modelcontextprotocol/sdk` - MCP protocol implementation
160
+ - `express` - HTTP server
161
+ - `zod` - Schema validation
162
+ - `dotenv` - Environment variables
163
+ - `vite` - Web component bundling
164
+
165
+ ## License
166
+
167
+ MIT
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/web/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {
22
+ "@manifest": "https://ui.manifest.build/r/{name}.json"
23
+ }
24
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "manfiest-starter",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx src/server.ts",
8
+ "dev:watch": "tsx --watch src/server.ts",
9
+ "build": "tsc && vite build",
10
+ "start": "NODE_ENV=production node dist/server.js"
11
+ },
12
+ "keywords": [],
13
+ "author": "MNFST, Inc.",
14
+ "license": "MIT",
15
+ "description": "Manifest starter project for ChatGPT",
16
+ "devDependencies": {
17
+ "@tailwindcss/vite": "^4.1.8",
18
+ "@types/express": "^5.0.6",
19
+ "@types/react": "^19.1.8",
20
+ "@types/react-dom": "^19.1.6",
21
+ "@vitejs/plugin-react": "^4.5.2",
22
+ "tailwindcss": "^4.1.8",
23
+ "tsx": "^4.21.0",
24
+ "typescript": "^5.9.3",
25
+ "vite": "^7.3.0"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.12.1",
29
+ "@radix-ui/react-checkbox": "^1.3.3",
30
+ "@radix-ui/react-slot": "^1.2.4",
31
+ "class-variance-authority": "^0.7.1",
32
+ "clsx": "^2.1.1",
33
+ "dotenv": "^17.2.3",
34
+ "express": "^5.1.0",
35
+ "lucide-react": "^0.511.0",
36
+ "react": "^19.1.0",
37
+ "react-dom": "^19.1.0",
38
+ "tailwind-merge": "^3.3.1",
39
+ "vite-plugin-chatgpt-widgets": "^0.0.10",
40
+ "zod": "^3.25.76"
41
+ }
42
+ }
@@ -0,0 +1,131 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import type { WidgetInfo } from 'vite-plugin-chatgpt-widgets'
3
+ import { z } from 'zod'
4
+
5
+ export interface Pokemon {
6
+ id: number
7
+ name: string
8
+ image: string
9
+ types: string[]
10
+ height: number
11
+ weight: number
12
+ }
13
+
14
+ async function fetchPokemons(limit: number = 12): Promise<Pokemon[]> {
15
+ const response = await fetch(
16
+ `https://pokeapi.co/api/v2/pokemon?limit=${limit}`
17
+ )
18
+ const data = await response.json()
19
+
20
+ const pokemons = await Promise.all(
21
+ data.results.map(async (pokemon: { name: string; url: string }) => {
22
+ const detailResponse = await fetch(pokemon.url)
23
+ const detail = await detailResponse.json()
24
+
25
+ return {
26
+ id: detail.id,
27
+ name: detail.name,
28
+ image:
29
+ detail.sprites.other['official-artwork'].front_default ||
30
+ detail.sprites.front_default,
31
+ types: detail.types.map(
32
+ (t: { type: { name: string } }) => t.type.name
33
+ ),
34
+ height: detail.height,
35
+ weight: detail.weight
36
+ }
37
+ })
38
+ )
39
+
40
+ return pokemons
41
+ }
42
+
43
+ /**
44
+ * Registers the Pokemon List flow with the MCP server.
45
+ */
46
+ export function registerPokemonFlow(
47
+ server: McpServer,
48
+ widgets: WidgetInfo[]
49
+ ): void {
50
+ const uiVersion = 'v1'
51
+ const resourceUri = `ui://pokemon-list.html?${uiVersion}`
52
+
53
+ // Find the PokemonList widget
54
+ const pokemonWidget = widgets.find((w) => w.name === 'PokemonList')
55
+
56
+ if (!pokemonWidget) {
57
+ console.warn('PokemonList widget not found - skipping flow registration')
58
+ return
59
+ }
60
+
61
+ server.registerResource('pokemon-list', resourceUri, {}, async () => ({
62
+ contents: [
63
+ {
64
+ uri: resourceUri,
65
+ mimeType: 'text/html+skybridge',
66
+ text: pokemonWidget.content,
67
+ _meta: { 'openai/widgetPrefersBorder': false }
68
+ }
69
+ ]
70
+ }))
71
+
72
+ server.registerTool(
73
+ 'listPokemons',
74
+ {
75
+ title: 'Pokemon List',
76
+ description: `Display a list of Pokemon in an interactive carousel.
77
+
78
+ WHEN TO USE:
79
+ - When the user asks to see a list of Pokemon
80
+ - When the user wants to browse Pokemon
81
+ - When the user asks "show me some Pokemon"
82
+
83
+ The tool fetches Pokemon data from the PokeAPI and displays them in a beautiful carousel format.`,
84
+ inputSchema: z.object({
85
+ limit: z
86
+ .number()
87
+ .min(1)
88
+ .max(50)
89
+ .optional()
90
+ .default(12)
91
+ .describe('Number of Pokemon to fetch (1-50, default: 12)')
92
+ }),
93
+ _meta: {
94
+ 'openai/outputTemplate': resourceUri,
95
+ 'openai/toolInvocation/invoking': 'Fetching Pokemon...',
96
+ 'openai/toolInvocation/invoked': 'Pokemon list ready!'
97
+ }
98
+ },
99
+ async (args: { limit?: number }) => {
100
+ console.log('listPokemons called with args:', JSON.stringify(args))
101
+ const limit = args.limit ?? 12
102
+ console.log('Using limit:', limit)
103
+
104
+ try {
105
+ const pokemons = await fetchPokemons(limit)
106
+
107
+ return {
108
+ content: [
109
+ {
110
+ type: 'text' as const,
111
+ text: `Found ${pokemons.length} Pokemon! Browse through the carousel to see them all.`
112
+ }
113
+ ],
114
+ structuredContent: {
115
+ pokemons
116
+ }
117
+ }
118
+ } catch (error) {
119
+ return {
120
+ content: [
121
+ {
122
+ type: 'text' as const,
123
+ text: `Failed to fetch Pokemon: ${error instanceof Error ? error.message : 'Unknown error'}`
124
+ }
125
+ ],
126
+ isError: true
127
+ }
128
+ }
129
+ }
130
+ )
131
+ }
@@ -0,0 +1,165 @@
1
+ import 'dotenv/config'
2
+ import type { IncomingMessage } from 'node:http'
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
5
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
6
+ import express from 'express'
7
+ import { getWidgets, type WidgetInfo } from 'vite-plugin-chatgpt-widgets'
8
+ import type { ViteDevServer } from 'vite'
9
+ import { registerPokemonFlow } from './flows/list-pokemons.flow.js'
10
+
11
+ const isDev = process.env.NODE_ENV !== 'production'
12
+ const port = Number(process.env.PORT) || 3000
13
+
14
+ // Session storage
15
+ interface SessionData {
16
+ transport: StreamableHTTPServerTransport
17
+ }
18
+ const sessions = new Map<string, SessionData>()
19
+
20
+ // Widget storage - populated at startup
21
+ let widgets: WidgetInfo[] = []
22
+
23
+ function createServer() {
24
+ const server = new McpServer({
25
+ name: 'Manifest Starter',
26
+ version: '0.0.1'
27
+ })
28
+
29
+ registerPokemonFlow(server, widgets)
30
+
31
+ return server
32
+ }
33
+
34
+ async function main() {
35
+ const app = express()
36
+
37
+ // Create Vite dev server in development mode
38
+ let viteDevServer: ViteDevServer | null = null
39
+
40
+ if (isDev) {
41
+ const { createServer } = await import('vite')
42
+ viteDevServer = await createServer({
43
+ server: { middlewareMode: true },
44
+ appType: 'custom'
45
+ })
46
+ // Use Vite's middleware for HMR and asset serving
47
+ app.use(viteDevServer.middlewares)
48
+ }
49
+
50
+ // Load widgets
51
+ if (isDev && viteDevServer) {
52
+ widgets = await getWidgets('src/web', { devServer: viteDevServer })
53
+ console.log(`Loaded ${widgets.length} widget(s) in development mode`)
54
+ } else {
55
+ widgets = await getWidgets('src/web', {
56
+ manifestPath: 'dist/web/.vite/manifest.json'
57
+ })
58
+ console.log(`Loaded ${widgets.length} widget(s) from production build`)
59
+ }
60
+
61
+ // Log available widgets
62
+ for (const widget of widgets) {
63
+ console.log(` - ${widget.name} (${widget.source})`)
64
+ }
65
+
66
+ app.use(express.json())
67
+
68
+ // CORS headers for all responses
69
+ app.use((_req, res, next) => {
70
+ res.header('Access-Control-Allow-Origin', '*')
71
+ res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
72
+ res.header(
73
+ 'Access-Control-Allow-Headers',
74
+ 'Content-Type, Accept, Authorization, mcp-session-id'
75
+ )
76
+ res.header(
77
+ 'Access-Control-Expose-Headers',
78
+ 'mcp-session-id, WWW-Authenticate'
79
+ )
80
+ next()
81
+ })
82
+
83
+ // Handle CORS preflight
84
+ app.options('/mcp', (_req, res) => {
85
+ res.sendStatus(204)
86
+ })
87
+
88
+ app.post('/mcp', async (req, res, next) => {
89
+ const sessionId = req.headers['mcp-session-id'] as string | undefined
90
+
91
+ // Existing session
92
+ if (sessionId && sessions.has(sessionId)) {
93
+ const session = sessions.get(sessionId)!
94
+ await session.transport
95
+ .handleRequest(req as unknown as IncomingMessage, res, req.body)
96
+ .catch(next)
97
+ return
98
+ }
99
+
100
+ // New session - create transport and server
101
+ const transport = new StreamableHTTPServerTransport({
102
+ sessionIdGenerator: () => crypto.randomUUID(),
103
+ enableJsonResponse: true
104
+ })
105
+
106
+ const server = createServer()
107
+
108
+ transport.onclose = () => {
109
+ const sid = (transport as unknown as { sessionId?: string }).sessionId
110
+ if (sid) sessions.delete(sid)
111
+ }
112
+
113
+ await server.connect(transport as Transport)
114
+ await transport
115
+ .handleRequest(req as unknown as IncomingMessage, res, req.body)
116
+ .catch(next)
117
+
118
+ // Store session after successful initialization
119
+ const newSessionId = (transport as unknown as { sessionId?: string })
120
+ .sessionId
121
+ if (newSessionId) {
122
+ sessions.set(newSessionId, { transport })
123
+ }
124
+ })
125
+
126
+ // Handle GET for SSE streams
127
+ app.get('/mcp', async (req, res, next) => {
128
+ const sessionId = req.headers['mcp-session-id'] as string | undefined
129
+
130
+ if (!sessionId || !sessions.has(sessionId)) {
131
+ res.status(400).json({ error: 'Invalid or missing session ID' })
132
+ return
133
+ }
134
+
135
+ const session = sessions.get(sessionId)!
136
+ await session.transport
137
+ .handleRequest(req as unknown as IncomingMessage, res)
138
+ .catch(next)
139
+ })
140
+
141
+ // Handle DELETE for session cleanup
142
+ app.delete('/mcp', async (req, res) => {
143
+ const sessionId = req.headers['mcp-session-id'] as string | undefined
144
+
145
+ if (sessionId && sessions.has(sessionId)) {
146
+ const session = sessions.get(sessionId)!
147
+ await session.transport.close()
148
+ sessions.delete(sessionId)
149
+ }
150
+
151
+ res.status(204).end()
152
+ })
153
+
154
+ app.listen(port, '0.0.0.0', () => {
155
+ console.log(`MCP server listening on http://localhost:${port}/mcp`)
156
+ if (isDev) {
157
+ console.log('Vite HMR enabled - widget changes will hot reload')
158
+ }
159
+ })
160
+ }
161
+
162
+ main().catch((err) => {
163
+ console.error('Failed to start server:', err)
164
+ process.exit(1)
165
+ })
@@ -0,0 +1,125 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { BlogPostList } from '@/components/blog-post-list'
3
+ import type { BlogPost } from '@/components/blog-post-card'
4
+
5
+ interface Pokemon {
6
+ id: number
7
+ name: string
8
+ image: string
9
+ types: string[]
10
+ height: number
11
+ weight: number
12
+ }
13
+
14
+ interface StructuredContent {
15
+ pokemons: Pokemon[]
16
+ }
17
+
18
+ function pokemonToBlogPost(pokemon: Pokemon): BlogPost {
19
+ const capitalizedName =
20
+ pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)
21
+
22
+ return {
23
+ id: String(pokemon.id),
24
+ title: capitalizedName,
25
+ excerpt: `Height: ${pokemon.height / 10}m | Weight: ${pokemon.weight / 10}kg`,
26
+ coverImage: pokemon.image,
27
+ author: {
28
+ name: `#${String(pokemon.id).padStart(3, '0')}`,
29
+ avatar: pokemon.image
30
+ },
31
+ publishedAt: new Date().toISOString(),
32
+ readTime: pokemon.types.join(', '),
33
+ tags: pokemon.types.map((t) => t.charAt(0).toUpperCase() + t.slice(1)),
34
+ category: pokemon.types[0]
35
+ ? pokemon.types[0].charAt(0).toUpperCase() + pokemon.types[0].slice(1)
36
+ : 'Normal'
37
+ }
38
+ }
39
+
40
+ export default function PokemonList() {
41
+ const [posts, setPosts] = useState<BlogPost[]>([])
42
+ const [loading, setLoading] = useState(true)
43
+
44
+ useEffect(() => {
45
+ console.log('window.openai:', window.openai)
46
+ console.log('structuredContent:', window.openai?.content?.structuredContent)
47
+
48
+ // Get structured content from OpenAI
49
+ if (window.openai?.content?.structuredContent) {
50
+ const content = window.openai.content
51
+ .structuredContent as StructuredContent
52
+ console.log('Using structuredContent, pokemons count:', content.pokemons?.length)
53
+ if (content.pokemons) {
54
+ setPosts(content.pokemons.map(pokemonToBlogPost))
55
+ }
56
+ setLoading(false)
57
+ } else {
58
+ // Fallback: fetch directly if not in OpenAI context
59
+ console.log('No structuredContent, falling back to direct fetch')
60
+ fetchPokemons()
61
+ }
62
+ }, [])
63
+
64
+ async function fetchPokemons() {
65
+ try {
66
+ const response = await fetch(
67
+ 'https://pokeapi.co/api/v2/pokemon?limit=12'
68
+ )
69
+ const data = await response.json()
70
+
71
+ const pokemons = await Promise.all(
72
+ data.results.map(
73
+ async (pokemon: { name: string; url: string }) => {
74
+ const detailResponse = await fetch(pokemon.url)
75
+ const detail = await detailResponse.json()
76
+
77
+ return {
78
+ id: detail.id,
79
+ name: detail.name,
80
+ image:
81
+ detail.sprites.other['official-artwork'].front_default ||
82
+ detail.sprites.front_default,
83
+ types: detail.types.map(
84
+ (t: { type: { name: string } }) => t.type.name
85
+ ),
86
+ height: detail.height,
87
+ weight: detail.weight
88
+ }
89
+ }
90
+ )
91
+ )
92
+
93
+ setPosts(pokemons.map(pokemonToBlogPost))
94
+ } catch (error) {
95
+ console.error('Failed to fetch Pokemon:', error)
96
+ } finally {
97
+ setLoading(false)
98
+ }
99
+ }
100
+
101
+ const handleReadMore = (post: BlogPost) => {
102
+ const pokemonName = post.title.toLowerCase()
103
+ window.openai?.sendFollowUpMessage?.(`Tell me more about ${pokemonName}`)
104
+ }
105
+
106
+ if (loading) {
107
+ return (
108
+ <div className="flex items-center justify-center p-8">
109
+ <div className="text-muted-foreground">Loading Pokemon...</div>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ return (
115
+ <div className="p-4">
116
+ <BlogPostList
117
+ posts={posts}
118
+ variant="carousel"
119
+ showAuthor={true}
120
+ showCategory={true}
121
+ onReadMore={handleReadMore}
122
+ />
123
+ </div>
124
+ )
125
+ }