├── .dockerignore
├── .github
└── workflows
│ └── docker-publish.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.config.ts
├── bun.lock
├── docker-compose.yml
├── docs
├── DOCKER.md
└── SELFHOST.md
├── instant.perms.ts
├── instant.schema.ts
├── package.json
├── patches
└── nitropack@2.10.4.patch
├── postcss.config.cjs
├── public
├── banner.png
├── favicon.svg
├── gaussian_noise.png
├── icon.jpg
├── logo.svg
├── name.svg
├── og.png
├── profile.svg
├── robots.txt
├── screenshot.jpg
├── screenshot2.jpg
├── screenshot3.jpg
├── sitemap.xml
└── whitehouse.jpg
├── src
├── app.css
├── app.tsx
├── client
│ ├── accept.ts
│ ├── database.tsx
│ └── utils.tsx
├── components
│ ├── AIComp.tsx
│ ├── BlockComp.tsx
│ ├── BuyComp.tsx
│ ├── BuySellComp.tsx
│ ├── CheckBox.tsx
│ ├── CheckboxItem.tsx
│ ├── IconComp.tsx
│ ├── Image.tsx
│ ├── InfiniteScroll.tsx
│ ├── MarketCard.tsx
│ ├── MarketImage.tsx
│ ├── MarketSocialComp.tsx
│ ├── Markets.tsx
│ ├── MartketChart.tsx
│ ├── Nav.tsx
│ ├── NewsItem.tsx
│ ├── OptionImage.tsx
│ ├── OptionItem.tsx
│ ├── ProfileImage.tsx
│ ├── SellComp.tsx
│ ├── Spinner.tsx
│ ├── TranslatingGroup.tsx
│ └── buysell
│ │ └── Header.tsx
├── entry-client.tsx
├── entry-server.tsx
├── routes
│ ├── [...404].tsx
│ ├── api
│ │ ├── alive
│ │ │ └── index.tsx
│ │ ├── complete
│ │ │ └── index.ts
│ │ ├── history__options
│ │ │ └── [id]
│ │ │ │ └── index.tsx
│ │ ├── markets
│ │ │ └── [id]
│ │ │ │ └── vote
│ │ │ │ └── index.ts
│ │ ├── options
│ │ │ └── [id]
│ │ │ │ └── action
│ │ │ │ └── index.tsx
│ │ └── profiles
│ │ │ └── jwt
│ │ │ └── index.tsx
│ ├── index.tsx
│ ├── market
│ │ └── [id].tsx
│ └── profile
│ │ └── [id].tsx
├── server
│ ├── chat-utils.ts
│ └── utils.ts
├── shared
│ ├── BLOCKS.json
│ ├── tools
│ │ ├── createMarket.ts
│ │ ├── index.ts
│ │ ├── searchImages.ts
│ │ ├── searchWeb.ts
│ │ └── utils.ts
│ └── utils.tsx
└── types
│ ├── brave_search_image.d.ts
│ ├── brave_search_news.d.ts
│ ├── brave_search_web.d.ts
│ ├── global.d.ts
│ └── window.d.ts
├── tailwind.config.cjs
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | *.env
4 | .DS_Store
5 | .vscode
6 | .git
7 |
--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push Docker Image
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | push_to_docker_hub:
10 | name: Build and Push to Docker Hub
11 | runs-on: ubuntu-latest
12 | permissions:
13 | contents: read
14 | packages: write
15 | attestations: write
16 | id-token: write
17 |
18 | steps:
19 | - name: Check out repository
20 | uses: actions/checkout@v4
21 |
22 | - name: Log in to Docker Hub
23 | uses: docker/login-action@v3
24 | with:
25 | username: ${{ secrets.DOCKER_USERNAME }}
26 | password: ${{ secrets.DOCKER_PASSWORD }}
27 |
28 | - name: Extract metadata (tags, labels)
29 | id: meta
30 | uses: docker/metadata-action@v5
31 | with:
32 | images: implyapp/imply
33 |
34 | - name: Build and push Docker image
35 | id: docker_build
36 | uses: docker/build-push-action@v5
37 | with:
38 | context: .
39 | file: ./Dockerfile
40 | push: true
41 | tags: |
42 | implyapp/imply:latest
43 | labels: ${{ steps.meta.outputs.labels }}
44 |
45 | - name: Generate artifact attestation
46 | uses: actions/attest-build-provenance@v2
47 | with:
48 | subject-name: implyapp/imply
49 | subject-digest: ${{ steps.docker_build.outputs.digest }}
50 |
51 | - name: Trigger Render Deploy
52 | run: |
53 | curl -X POST "https://api.render.com/deploy/srv-cugkir23esus73b1s9d0?key=${{ secrets.RENDER_DEPLOY_KEY }}&imgURL=docker.io%2Fimplyapp%2Fimply%3Alatest"
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | dist
3 | .solid
4 | .output
5 | .vercel
6 | .netlify
7 | .vinxi
8 | app.config.timestamp_*.js
9 |
10 | # Environment
11 | .env
12 | .env*.local
13 |
14 | # dependencies
15 | /node_modules
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | *.launch
22 | .settings/
23 |
24 | # Temp
25 | gitignore
26 |
27 | # System Files
28 | .DS_Store
29 | Thumbs.db
30 |
31 | .wrangler
32 | wrangler.json
33 | wrangler.toml
34 | .*.vars
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # use the official Bun image
2 | # see all versions at https://hub.docker.com/r/oven/bun/tags
3 | FROM oven/bun:latest as base
4 | WORKDIR /usr/src/app
5 |
6 | # install production dependencies
7 | FROM base AS install
8 | RUN mkdir -p /temp/prod
9 | COPY package.json bun.lock /temp/prod/
10 | COPY patches/ /temp/prod/patches/
11 | RUN cd /temp/prod && bun install --frozen-lockfile
12 |
13 | # copy production dependencies and source code into final image
14 | FROM base AS release
15 | COPY --from=install /temp/prod/node_modules node_modules
16 | COPY . .
17 |
18 | # [optional] tests & build (if needed for production build)
19 | ENV NODE_ENV=production
20 | RUN bun run build
21 |
22 | # run the app
23 | USER bun
24 | EXPOSE 3000/tcp
25 | ENTRYPOINT [ "bun", "run", "start" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Tri Nguyen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 | ## Imply
12 |
13 | A prediction market is like a stock market, but instead of buying and selling company shares, people trade guesses about future events. The price of each guess shows how likely people think that event is to happen. If you predict correctly, you make money; if you're wrong, you lose. This helps gather the opinions of many people to make better predictions.
14 |
15 | Imply make prediction markets easy to use & understand. The app is live here [https://imply.app](https://imply.app).
16 |
17 | ### Currently working on:
18 | - [x] 🤖 AI Agents
19 | - [x] 📚 can research & create prediction markets
20 | - [ ] 🔀 can merge similar markets
21 | - [ ] 🔍 can resolve markets (i.e. verifying sources)
22 | - [ ] 💼 can buy/sell shares on users' behalf
23 | - [x] 📈 Prediction markets
24 | - [x] ⚙️ Automated Market Markers (CPMM)
25 | - [x] ⏱️ Real-time price update
26 | - [ ] 🖥️ Simplified trading UI/UX
27 |
28 | ### Quick Demo (Youtube)
29 |
30 | [](https://www.youtube.com/watch?v=x3VCd4FStJU)
31 |
32 | ## Self-Hosting
33 |
34 | To self-host Imply, follow the [self-hosting guide](/docs/SELFHOST.md).
35 |
36 | ## License
37 |
38 | Imply is released under the **MIT License**. See [LICENSE](LICENSE) for details.
39 |
40 | ## Support
41 |
42 | For any questions or support, join our [Discord](https://discord.gg/XakeDSQSxc) or email **hi@imply.app**.
43 |
--------------------------------------------------------------------------------
/app.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@solidjs/start/config";
2 |
3 | export default defineConfig({
4 | server: {
5 | preset: 'bun',
6 | rollupConfig: {
7 | external: ["jose", "@instantdb/core", "@instantdb/admin"],
8 | }
9 | }
10 | });
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | app:
5 | image: imply
6 | ports:
7 | - "3000:3000"
8 | env_file:
9 | - .env
10 | restart: unless-stopped
11 |
--------------------------------------------------------------------------------
/docs/DOCKER.md:
--------------------------------------------------------------------------------
1 | # Quick Guide to Running **Imply** with Docker
2 |
3 | Some useful Docker commands:
4 |
5 | 1. Build the Docker image for **Imply**.
6 |
7 | ```sh
8 | docker build -t docker.io/implyapp/imply .
9 | ```
10 |
11 | 2. Use the following to start the application:
12 |
13 | ```sh
14 | docker-compose up
15 | ```
16 |
17 | 3. To run **Imply** in a detached container:
18 |
19 | ```sh
20 | docker run -d --name imply --env-file .env -p 3000:3000 implyapp/imply
21 | ```
22 |
--------------------------------------------------------------------------------
/docs/SELFHOST.md:
--------------------------------------------------------------------------------
1 | # Self-Hosting Guide
2 |
3 | This document outlines the steps required to run **Imply**, either via Docker (recommended) or by building yourself. Both methods require setting up the `.env` file.
4 |
5 | ## Create an `.env` file with the following keys:
6 |
7 | ```sh
8 | BUN_VERSION=
9 |
10 | # AI
11 | OPENAI_API_KEY=
12 | OPENAI_BASE_URL=
13 | OPENAI_MODEL=
14 | REASONING_MODEL=
15 |
16 | # Internet
17 | BRAVE_SEARCH_API_KEY=
18 |
19 | # Database
20 | JWT_SECRET_KEY=
21 | INSTANTDB_APP_ID=
22 | INSTANT_APP_ADMIN_TOKEN=
23 |
24 | # Analytics
25 | POSTHOG_TOKEN=
26 | ```
27 |
28 | ### 🔑 Key Details:
29 |
30 | 1. **BUN_VERSION**: Required for using Bun with Cloudflare Pages. Set it to `1.1.38` if unsure.
31 | 2. **InstantDB**: Imply uses **InstantDB**. Since InstantDB is open-source, you can self-host it or use their free & unlimited cloud version. To set it up:
32 |
33 | ```sh
34 | # In the root folder
35 | npx instant-cli@latest push schema
36 | # Choose "Create a new app" and push the schema
37 | ```
38 |
39 | Afterward, visit the **InstantDB dashboard** to get `INSTANTDB_APP_ID` and `INSTANT_APP_ADMIN_TOKEN`.
40 |
41 | 3. **JWT_SECRET_KEY**: Generate this key with the following command:
42 |
43 | ```sh
44 | bun -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
45 | ```
46 |
47 | 4. **BRAVE_SEARCH_API_KEY**: Get a free key from [Brave](https://brave.com/search/api/).
48 |
49 | 5. **OPENAI_API_KEY**: API key to use with OpenAI SDK. **OPENAI_MODEL** and **OPENAI_BASE_URL** are optional. In production, we use Open Router to mix & match multiple models, so our `.env` looks like this:
50 |
51 | ```sh
52 | OPENAI_API_KEY=
53 | OPENAI_BASE_URL=https://openrouter.ai/api/v1
54 | OPENAI_MODEL=openai/gpt-4o-mini
55 | REASONING_MODEL=deepseek/deepseek-r1
56 | ```
57 |
58 | 6. **POSTHOG_TOKEN**: This is optional & used for analytics purposes.
59 |
60 | ## Method 1: Via Docker
61 |
62 | The easiest way to get Imply running on your machine is via Docker.
63 |
64 | ```sh
65 | docker run -d --name imply --env-file .env -p 3000:3000 implyapp/imply
66 | ```
67 |
68 | ## Method 2: Cloning and Building
69 |
70 | Imply is a web app built using the JavaScript framework **Solid Start**.
71 | If you don't have **Bun** installed, we recommend doing so.
72 |
73 | ### 1. Clone & Install Dependencies
74 |
75 | ```sh
76 | git clone https://github.com/tri2820/imply
77 | cd imply
78 | bun i
79 | ```
80 |
81 | ### 2. Add the `.env` file
82 |
83 | Make sure you have the `.env` file created as described in the previous section & put it in the root folder.
84 |
85 | ### 3. Build & Run the App
86 |
87 | ```sh
88 | bun run build
89 | bun run start
90 | ```
91 |
--------------------------------------------------------------------------------
/instant.perms.ts:
--------------------------------------------------------------------------------
1 | // Docs: https://www.instantdb.com/docs/permissions
2 |
3 | import type { InstantRules } from "@instantdb/core";
4 |
5 | const rules = {
6 | /**
7 | * Welcome to Instant's permission system!
8 | * Right now your rules are empty. To start filling them in, check out the docs:
9 | * https://www.instantdb.com/docs/permissions
10 | *
11 | * Here's an example to give you a feel:
12 | * posts: {
13 | * allow: {
14 | * view: "true",
15 | * create: "isOwner",
16 | * update: "isOwner",
17 | * delete: "isOwner",
18 | * },
19 | * bind: ["isOwner", "auth.id != null && auth.id == data.ownerId"],
20 | * },
21 | */
22 | } satisfies InstantRules;
23 |
24 | export default rules;
25 |
--------------------------------------------------------------------------------
/instant.schema.ts:
--------------------------------------------------------------------------------
1 | import { i } from "@instantdb/core";
2 |
3 | const _schema = i.schema({
4 | // This section lets you define entities: think `posts`, `comments`, etc
5 | // Take a look at the docs to learn more:
6 | // https://www.instantdb.com/docs/modeling-data#2-attributes
7 | entities: {
8 | $users: i.entity({
9 | email: i.string().unique().indexed(),
10 | }),
11 | shares: i.entity({
12 | type: i.string(), // Yes or No
13 | reserve: i.number(),
14 | }),
15 | options: i.entity({
16 | name: i.string(),
17 | color: i.string(),
18 | image: i.string(),
19 | }),
20 | markets: i.entity({
21 | name: i.string(),
22 | description: i.string(),
23 | image: i.string(),
24 | allow_multiple_correct: i.boolean(),
25 | created_at: i.date(),
26 | resolve_at: i.date(),
27 | stop_trading_at: i.date(),
28 | rule: i.string(),
29 |
30 | // social
31 | // faster (don't have to aggregate)
32 | // Only indexed and type-checked attrs can be used to order by.
33 | num_votes: i.number().indexed(),
34 | }),
35 | holdings: i.entity({
36 | amount: i.number(),
37 | updated_at: i.date(),
38 | }),
39 | profiles: i.entity({
40 | name: i.string(),
41 | avatar_src: i.string(),
42 | usd: i.number(),
43 | }),
44 | history__options: i.entity({
45 | option_id: i.string(),
46 | created_at: i.date(),
47 | yesProb: i.number(),
48 | }),
49 | conversations: i.entity({
50 | name: i.string(),
51 | }),
52 | blocks: i.entity({
53 | created_at: i.date(),
54 | updated_at: i.date(),
55 | content: i.json(),
56 | role: i.string(),
57 | agent_step: i.string().optional(),
58 | }),
59 |
60 | votes: i.entity({
61 | isUpvote: i.boolean(),
62 | })
63 | },
64 | // You can define links here.
65 | // For example, if `posts` should have many `comments`.
66 | // More in the docs:
67 | // https://www.instantdb.com/docs/modeling-data#3-links
68 | links: {
69 | profile_votes: {
70 | forward: {
71 | on: "profiles",
72 | has: "many",
73 | label: "votes",
74 | },
75 | reverse: {
76 | on: "votes",
77 | has: "one",
78 | label: "profile",
79 | },
80 | },
81 | market_votes: {
82 | forward: {
83 | on: "markets",
84 | has: "many",
85 | label: "votes",
86 | },
87 | reverse: {
88 | on: "votes",
89 | has: "one",
90 | label: "market",
91 | },
92 | },
93 | options_shares: {
94 | forward: {
95 | on: "options",
96 | has: "many",
97 | label: "shares",
98 | },
99 | reverse: {
100 | on: "shares",
101 | has: "one",
102 | label: "option",
103 | },
104 | },
105 | markets_options: {
106 | forward: {
107 | on: "markets",
108 | has: "many",
109 | label: "options",
110 | },
111 | reverse: {
112 | on: "options",
113 | has: "one",
114 | label: "market",
115 | },
116 | },
117 | profiles_holdings: {
118 | forward: {
119 | on: "profiles",
120 | has: "many",
121 | label: "holdings",
122 | },
123 | reverse: {
124 | on: "holdings",
125 | has: "one",
126 | label: "profile",
127 | },
128 | },
129 | holdings_shares: {
130 | forward: {
131 | on: "holdings",
132 | has: "one",
133 | label: "share",
134 | },
135 | reverse: {
136 | on: "shares",
137 | has: "many",
138 | label: "holdings",
139 | },
140 | },
141 | profile_blocks: {
142 | forward: {
143 | on: "profiles",
144 | has: "many",
145 | label: "blocks",
146 | },
147 | reverse: {
148 | on: "blocks",
149 | has: "one",
150 | label: "profile",
151 | },
152 | }
153 | },
154 | // If you use presence, you can define a room schema here
155 | // https://www.instantdb.com/docs/presence-and-topics#typesafety
156 | rooms: {},
157 | });
158 |
159 | // This helps Typescript display nicer intellisense
160 | type _AppSchema = typeof _schema;
161 | interface AppSchema extends _AppSchema { }
162 | const schema: AppSchema = _schema;
163 |
164 | export type { AppSchema };
165 | export default schema;
166 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imply",
3 | "type": "module",
4 | "scripts": {
5 | "dev": "vinxi dev",
6 | "build": "vinxi build",
7 | "start": "bunx --bun vinxi start"
8 | },
9 | "dependencies": {
10 | "@fontsource-variable/lexend": "^5.1.2",
11 | "@fontsource/poppins": "^5.1.1",
12 | "@instantdb/admin": "^0.17.9",
13 | "@instantdb/core": "^0.17.9",
14 | "@solidjs/router": "^0.15.0",
15 | "@solidjs/start": "^1.0.11",
16 | "date-fns": "^4.1.0",
17 | "jose": "^5.9.6",
18 | "json-stream-es": "^1.2.1",
19 | "lightweight-charts": "^4.2.2",
20 | "marked": "^15.0.6",
21 | "openai": "^4.80.0",
22 | "posthog-js": "^1.215.3",
23 | "solid-icons": "^1.1.0",
24 | "solid-js": "^1.9.2",
25 | "streaming-iterables": "^8.0.1",
26 | "vinxi": "^0.4.3",
27 | "zod": "^3.24.1",
28 | "zod-to-json-schema": "^3.24.1"
29 | },
30 | "devDependencies": {
31 | "@tailwindcss/typography": "^0.5.16",
32 | "autoprefixer": "^10.4.19",
33 | "postcss": "^8.4.38",
34 | "tailwindcss": "^3.4.3"
35 | },
36 | "overrides": {
37 | "vite": "5.4.10"
38 | },
39 | "engines": {
40 | "node": ">=18"
41 | },
42 | "patchedDependencies": {
43 | "nitropack@2.10.4": "patches/nitropack@2.10.4.patch"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/patches/nitropack@2.10.4.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/nitropack/.bun-tag-d7ccf2a00e004218 b/.bun-tag-d7ccf2a00e004218
2 | new file mode 100644
3 | index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
4 | diff --git a/dist/presets/bun/runtime/bun.mjs b/dist/presets/bun/runtime/bun.mjs
5 | index e46d1616dc6d6efa473e2d53a0884efc83fd7867..209bf44fbf2ef69763d13e055f0e6bee84e83857 100644
6 | --- a/dist/presets/bun/runtime/bun.mjs
7 | +++ b/dist/presets/bun/runtime/bun.mjs
8 | @@ -5,6 +5,7 @@ import wsAdapter from "crossws/adapters/bun";
9 | const nitroApp = useNitroApp();
10 | const ws = import.meta._websocket ? wsAdapter(nitroApp.h3App.websocket) : void 0;
11 | const server = Bun.serve({
12 | + idleTimeout: 0,
13 | port: process.env.NITRO_PORT || process.env.PORT || 3e3,
14 | websocket: import.meta._websocket ? ws.websocket : void 0,
15 | async fetch(req, server2) {
16 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tri2820/imply/a4f7d68dd4e35af68010af36ad28faf4210eb439/public/banner.png
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/gaussian_noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tri2820/imply/a4f7d68dd4e35af68010af36ad28faf4210eb439/public/gaussian_noise.png
--------------------------------------------------------------------------------
/public/icon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tri2820/imply/a4f7d68dd4e35af68010af36ad28faf4210eb439/public/icon.jpg
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/name.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tri2820/imply/a4f7d68dd4e35af68010af36ad28faf4210eb439/public/og.png
--------------------------------------------------------------------------------
/public/profile.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # robots.txt for imply.app
2 |
3 | User-agent: *
4 | Disallow: /admin/
5 | Disallow: /api/
6 | Disallow: /private/
7 | Disallow: /tmp/
8 | Allow: /
9 |
10 | # Sitemap
11 | Sitemap: https://imply.app/sitemap.xml
--------------------------------------------------------------------------------
/public/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tri2820/imply/a4f7d68dd4e35af68010af36ad28faf4210eb439/public/screenshot.jpg
--------------------------------------------------------------------------------
/public/screenshot2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tri2820/imply/a4f7d68dd4e35af68010af36ad28faf4210eb439/public/screenshot2.jpg
--------------------------------------------------------------------------------
/public/screenshot3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tri2820/imply/a4f7d68dd4e35af68010af36ad28faf4210eb439/public/screenshot3.jpg
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | https://imply.app/
6 | 2025-01-26
7 | weekly
8 | 1.0
9 |
10 |
11 |
39 |
40 |
--------------------------------------------------------------------------------
/public/whitehouse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tri2820/imply/a4f7d68dd4e35af68010af36ad28faf4210eb439/public/whitehouse.jpg
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background-rgb: 19, 19, 19;
7 | --foreground-rgb: 255, 255, 255;
8 | }
9 |
10 | body {
11 | /* font-family: "Inter", sans-serif; */
12 | font-family: 'Poppins', sans-serif;
13 | background: rgb(var(--background-rgb));
14 | color: rgb(var(--foreground-rgb));
15 |
16 | margin: 0;
17 | padding: 0;
18 | overflow-x: hidden;
19 | position: relative;
20 | }
21 |
22 | .grain {
23 | position: fixed;
24 | top: 0;
25 | left: 0;
26 | height: 100%;
27 | width: 100%;
28 | pointer-events: none;
29 | z-index: -10;
30 | transform: translateZ(0);
31 | }
32 |
33 | .grain::before {
34 | content: "";
35 | top: -10rem;
36 | left: -10rem;
37 | width: calc(100% + 20rem);
38 | height: calc(100% + 20rem);
39 | z-index: 9999;
40 | position: fixed;
41 | background-image: url(/gaussian_noise.png);
42 | opacity: 0.05;
43 | pointer-events: none;
44 | }
45 |
46 | .dots-bg {
47 | width: 100%;
48 | height: 100%;
49 | background-image: radial-gradient(circle, #404040 2px, transparent 1px);
50 | background-size: 60px 40px;
51 | z-index: -1;
52 | }
53 |
54 | .no-scrollbar {
55 | scrollbar-width: none;
56 | -ms-overflow-style: none;
57 | }
58 |
59 | .checkbox-no-style {
60 | -webkit-appearance: none;
61 | -moz-appearance: none;
62 | appearance: none;
63 | outline: none;
64 | border: none;
65 | background: none;
66 | }
67 |
68 | /* Chrome, Safari, Edge, Opera */
69 | input::-webkit-outer-spin-button,
70 | input::-webkit-inner-spin-button {
71 | -webkit-appearance: none;
72 | margin: 0;
73 | }
74 |
75 | /* Firefox */
76 | input[type=number] {
77 | -moz-appearance: textfield;
78 | }
79 |
80 | .table-container {
81 | @apply bg-neutral-900 rounded-lg border border-neutral-800 overflow-hidden;
82 | }
83 |
84 | .table-container thead {
85 | @apply text-left p-4;
86 | }
87 |
88 | .table-container tr {
89 | @apply border-b border-neutral-800;
90 | }
91 |
92 | .table-container td,
93 | .table-container th {
94 | @apply p-4;
95 | }
96 |
97 | .table-container th {
98 | @apply bg-neutral-800;
99 | }
100 |
101 |
102 | @keyframes translating {
103 | 0% {
104 | transform: translateX(0);
105 | }
106 |
107 | 100% {
108 | transform: translateX(-100%);
109 | }
110 | }
111 |
112 | .font-lexend {
113 | font-family: 'Lexend Variable', sans-serif;
114 | }
115 |
116 | span.avoidwrap {
117 | display: inline-block;
118 | }
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { Router } from "@solidjs/router";
2 | import { FileRoutes } from "@solidjs/start/router";
3 | import { onCleanup, onMount, Suspense } from "solid-js";
4 |
5 | import "./app.css";
6 |
7 | // Supports weights 100-900
8 | import "@fontsource-variable/lexend";
9 | import "@fontsource/poppins/100.css";
10 | import "@fontsource/poppins/200.css";
11 | import "@fontsource/poppins/300.css";
12 | import "@fontsource/poppins/400.css";
13 | import "@fontsource/poppins/500.css";
14 | import "@fontsource/poppins/600.css";
15 | import "@fontsource/poppins/700.css";
16 | import "@fontsource/poppins/800.css";
17 | import "@fontsource/poppins/900.css";
18 | import Nav from "./components/Nav";
19 | import { db } from "./client/database";
20 | import { setProfileSubscription } from "./client/utils";
21 | import posthog from "posthog-js";
22 |
23 | export default function App() {
24 | onMount(() => {
25 | if (!window.env.POSTHOG_TOKEN) return;
26 | console.log("connecting to posthog", window.env.POSTHOG_TOKEN);
27 | posthog.init(window.env.POSTHOG_TOKEN, {
28 | api_host: "https://eu.i.posthog.com",
29 | person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well
30 | });
31 | });
32 |
33 | function subscribeProfile(profile_id: string) {
34 | console.log("subscribeProfile", profile_id);
35 | return db.subscribeQuery(
36 | {
37 | profiles: {
38 | $: {
39 | where: {
40 | id: profile_id,
41 | },
42 | },
43 | holdings: {
44 | share: {
45 | // get the type
46 | },
47 | },
48 | },
49 | },
50 | (resp) => {
51 | console.log("profile sub resp", resp);
52 | setProfileSubscription(resp);
53 | }
54 | );
55 | }
56 |
57 | onMount(async () => {
58 | let unsub: Function;
59 | onCleanup(() => {
60 | unsub?.();
61 | });
62 |
63 | try {
64 | const resp = await fetch("/api/profiles/jwt", {
65 | method: "GET",
66 | });
67 | console.log("profile_jwt resp", resp);
68 | if (!resp.ok) throw new Error("fetch profile_jwt failed");
69 | const json: JWTResult = await resp.json();
70 | console.log("profile", json);
71 | unsub = subscribeProfile(json.profile_id);
72 | } catch (e) {
73 | console.error("error fetch profiles key", e);
74 | }
75 | });
76 |
77 | return (
78 | (
80 |
81 |
82 |
83 |
{props.children}
84 |
85 | )}
86 | >
87 |
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/src/client/accept.ts:
--------------------------------------------------------------------------------
1 |
2 | import { ToolName } from "~/shared/tools/utils";
3 | import { db } from "./database";
4 | import { blocks, profile, setBlocks } from "./utils";
5 | import { seededUUIDv4 } from "~/shared/utils";
6 |
7 | async function accept_content(content: NonNullable, agent_step: AgentStep) {
8 | let assistantBlock: AssistantBlock | undefined = undefined;
9 | if (content.started) {
10 | assistantBlock = {
11 | agent_step,
12 | id: content.started.id,
13 | role: "assistant",
14 | content: content.started.text,
15 | // created_at: content.started.created_at,
16 | // updated_at: content.started.created_at,
17 | created_at: new Date().toISOString(),
18 | updated_at: new Date().toISOString(),
19 | }
20 | }
21 |
22 | if (content.delta) {
23 | assistantBlock = blocks()[content.delta.id] as AssistantBlock;
24 | assistantBlock.content += content.delta.text;
25 | // assistantBlock.updated_at = content.delta.updated_at;
26 | assistantBlock.updated_at = new Date().toISOString()
27 | }
28 |
29 | if (content.done) {
30 | assistantBlock = blocks()[content.done.id] as AssistantBlock;
31 | assistantBlock.updated_at = new Date().toISOString()
32 | const instantdb_id = seededUUIDv4(assistantBlock.id);
33 | db.transact([db.tx.blocks[instantdb_id].update(assistantBlock).link({
34 | profile: profile()?.id
35 | })]);
36 | }
37 |
38 | if (assistantBlock) {
39 | setBlocks((blocks) => {
40 | return {
41 | ...blocks,
42 | [assistantBlock.id]: {
43 | ...assistantBlock
44 | },
45 | };
46 | });
47 | }
48 | }
49 |
50 |
51 | async function accept_reasoning(reasoning: NonNullable, agent_step: AgentStep) {
52 | let reasoningBlock: ReasoningBlock | undefined = undefined;
53 | if (reasoning.started) {
54 | reasoningBlock = {
55 | agent_step,
56 | id: reasoning.started.id,
57 | role: "reasoning",
58 | content: reasoning.started.text,
59 | // created_at: content.started.created_at,
60 | // updated_at: content.started.created_at,
61 | created_at: new Date().toISOString(),
62 | updated_at: new Date().toISOString(),
63 | }
64 | }
65 |
66 | if (reasoning.delta) {
67 | reasoningBlock = blocks()[reasoning.delta.id] as ReasoningBlock;
68 | reasoningBlock.content += reasoning.delta.text;
69 | // assistantBlock.updated_at = content.delta.updated_at;
70 | reasoningBlock.updated_at = new Date().toISOString()
71 | }
72 |
73 | if (reasoning.done) {
74 | reasoningBlock = blocks()[reasoning.done.id] as ReasoningBlock;
75 | reasoningBlock.updated_at = new Date().toISOString()
76 | const instantdb_id = seededUUIDv4(reasoningBlock.id);
77 | db.transact([db.tx.blocks[instantdb_id].update(reasoningBlock).link({
78 | profile: profile()?.id
79 | })]);
80 | }
81 |
82 | if (reasoningBlock) {
83 | setBlocks((blocks) => {
84 | return {
85 | ...blocks,
86 | [reasoningBlock.id]: {
87 | ...reasoningBlock
88 | },
89 | };
90 | });
91 | }
92 | }
93 |
94 |
95 |
96 | export async function accept_tool(tool: NonNullable, agent_step: AgentStep) {
97 | let toolBlock: ToolBlock | undefined = undefined;
98 |
99 | if (tool.started) {
100 | toolBlock = {
101 | agent_step,
102 | role: 'tool',
103 | id: tool.started.id,
104 | content: {
105 | doings: [],
106 | arguments_partial_str: '',
107 | name: tool.started.name as ToolName,
108 | },
109 | // created_at: tool.started.created_at,
110 | // updated_at: tool.started.created_at
111 | created_at: new Date().toISOString(),
112 | updated_at: new Date().toISOString()
113 | }
114 | }
115 |
116 |
117 | if (tool.delta) {
118 | toolBlock = blocks()[tool.delta.id] as ToolBlock;
119 | toolBlock.content.arguments_partial_str += tool.delta.arguments_delta;
120 | // toolBlock.updated_at = tool.delta.updated_at;
121 | toolBlock.updated_at = new Date().toISOString()
122 | }
123 |
124 |
125 | if (tool.done) {
126 | toolBlock = blocks()[tool.done.id] as ToolBlock
127 | toolBlock.content.arguments = tool.done.arguments;
128 | const instantdb_id = seededUUIDv4(toolBlock.id);
129 | db.transact([db.tx.blocks[instantdb_id].update(toolBlock).link({
130 | profile: profile()?.id
131 | })]);
132 | }
133 |
134 | if (toolBlock) {
135 | setBlocks((blocks) => {
136 | return {
137 | ...blocks,
138 | [toolBlock.id]: {
139 | ...toolBlock
140 | },
141 | };
142 | });
143 | }
144 |
145 | }
146 |
147 | export async function accept_tool_yield(tool: ToolYieldWithId, agent_step: AgentStep) {
148 | console.log('accept_tool_yield tool is toolYield', tool)
149 | let toolBlock: ToolBlock | undefined;
150 | if (tool.done) {
151 | toolBlock = blocks()[tool.id] as ToolBlock
152 | toolBlock.content.result = tool.done;
153 | const instantdb_id = seededUUIDv4(toolBlock.id);
154 | db.transact([db.tx.blocks[instantdb_id].update(toolBlock).link({
155 | profile: profile()?.id
156 | })]);
157 | } else if (tool.doing) {
158 | console.log('tool.doing', tool.doing)
159 | toolBlock = blocks()[tool.id] as ToolBlock
160 | toolBlock.content.doings = [...toolBlock.content.doings, tool.doing];
161 | }
162 |
163 | console.log('accept_tool_yield toolBlock', toolBlock)
164 | if (toolBlock) {
165 | setBlocks((blocks) => {
166 | return {
167 | ...blocks,
168 | [toolBlock.id]: {
169 | ...toolBlock
170 | },
171 | };
172 | });
173 | }
174 | }
175 |
176 | export async function accept(y: ChatStreamYield) {
177 | console.log('y', y.agent_step)
178 | if (y.reasoning) {
179 | accept_reasoning(y.reasoning, y.agent_step)
180 | }
181 |
182 | if (y.content) {
183 | accept_content(y.content, y.agent_step)
184 | }
185 |
186 | if (y.tool) {
187 | accept_tool(y.tool, y.agent_step)
188 | }
189 |
190 | if (y.tool_yield) {
191 | accept_tool_yield(y.tool_yield, y.agent_step)
192 | }
193 | }
--------------------------------------------------------------------------------
/src/client/database.tsx:
--------------------------------------------------------------------------------
1 | import { init } from "@instantdb/core";
2 | import schema, { AppSchema } from "../../instant.schema";
3 |
4 | // Initialize the database
5 | // ---------
6 | // @ts-ignore
7 | let db: ReturnType> = undefined;
8 | if (typeof window !== "undefined") {
9 | if (!window.env.INSTANTDB_APP_ID) throw new Error("INSTANTDB_APP_ID not set");
10 | db = init({
11 | appId: window.env.INSTANTDB_APP_ID,
12 | schema,
13 | devtool: false,
14 | });
15 | }
16 |
17 | export { db };
18 |
--------------------------------------------------------------------------------
/src/client/utils.tsx:
--------------------------------------------------------------------------------
1 | import { id } from "@instantdb/core";
2 | import {
3 | LastPriceAnimationMode,
4 | LineType,
5 | UTCTimestamp,
6 | } from "lightweight-charts";
7 | import { createSignal } from "solid-js";
8 | import {
9 | calcAttributes,
10 | createOption,
11 | triggerAddHistoryOption,
12 | } from "~/shared/utils";
13 | import { db } from "./database";
14 |
15 | export async function addMockMarket(num_option: number = 5) {
16 | const market_id = id();
17 |
18 | const options = Array.from({ length: num_option }, (_, i) =>
19 | createOption(db, `Option ${i + 1}`, Math.random())
20 | );
21 | const transactions: Parameters[0] = [
22 | ...options.flatMap((o) => o.transactions),
23 | db.tx.markets[market_id]
24 | .update({
25 | name: "Who will win the next election?",
26 | description: "Predict the winning candidate of the 2028 election. ",
27 | image: "",
28 | // The options are not independent
29 | // Only one option can win
30 | // Unlike "Who will attend the inauguration? (Barack Y/N, Trump Y/N, Biden Y/N)"
31 | allow_multiple_correct: false,
32 | created_at: new Date().toISOString(),
33 | resolve_at: new Date(
34 | Date.now() + 3 * 24 * 60 * 60 * 1000
35 | ).toISOString(),
36 | stop_trading_at: new Date(
37 | Date.now() + (3 - 1) * 24 * 60 * 60 * 1000
38 | ).toISOString(),
39 | rule: `The winner of the market is the candidate that wins the election according to Google News.\nIf a candidate drops out before the election, the option will be resolved as "No".\nIf the result of the election is disputed, the option for all candidatte will be resolved as "No".`,
40 | })
41 | .link({
42 | options: options.map((o) => o.option_id),
43 | }),
44 | ];
45 |
46 | await db.transact(transactions);
47 |
48 | // trigger api history__options
49 | const ps = options.map((o) => triggerAddHistoryOption(o.option_id));
50 | await Promise.all(ps);
51 | }
52 |
53 | export const [loadMarketsState, setLoadMarketsState] = createSignal<
54 | "idle" | "loading"
55 | >("idle");
56 | export async function loadMarkets() {
57 | setLoadMarketsState("loading");
58 | try {
59 | console.log("load markets");
60 | const r = marketResponses().at(-1);
61 | const lastCursor = r?.pageInfo?.markets?.endCursor;
62 | const resp = await db.queryOnce({
63 | markets: {
64 | options: {
65 | shares: {},
66 | },
67 | $: {
68 | first: 10,
69 | ...(lastCursor && { after: lastCursor }),
70 | order: {
71 | num_votes: "desc",
72 | },
73 | },
74 | },
75 | });
76 |
77 | console.log("loadMarkets resp", resp);
78 | setMarketResponses((prev) => [...prev, resp]);
79 | } catch (e) {}
80 | setLoadMarketsState("idle");
81 | }
82 |
83 | export const [blocks, setBlocks] = createSignal({});
84 | export const blocksToList = (blocks: Blocks) =>
85 | Object.values(blocks).toSorted(
86 | (a, b) =>
87 | new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
88 | );
89 | export const listBlocks = () => blocksToList(blocks());
90 |
91 | if (typeof window !== "undefined") {
92 | // @ts-ignore
93 | window.dev = {
94 | addMockMarket,
95 | setBlocks,
96 | blocks: () => {
97 | setBlocks((prev) => {
98 | console.log("blocks", prev);
99 | return prev;
100 | });
101 | },
102 | };
103 | }
104 |
105 | // Markets page
106 | export const [marketResponses, setMarketResponses] = createSignal<
107 | MarketResponse[]
108 | >([]);
109 | export const markets = () => {
110 | const ms = marketResponses().flatMap((m) => m.data.markets);
111 | return ms.map((m) => calcAttributes(m));
112 | };
113 | export const marketsHasNextPage = () => {
114 | const r = marketResponses().at(-1);
115 | const hasNextPage = r?.pageInfo?.markets?.hasNextPage;
116 | return hasNextPage;
117 | };
118 |
119 | // Universial
120 | export const [profileSubscription, setProfileSubscription] =
121 | createSignal();
122 | export const profile = () => {
123 | const s = profileSubscription();
124 | const p = s?.data?.profiles.at(0);
125 | return p;
126 | };
127 |
128 | // BuySelComp
129 |
130 | export const [optionId, setOptionId] = createSignal();
131 | export const [type, setType] = createSignal("yes");
132 |
133 | export const [marketSubscription, setMarketSubscription] =
134 | createSignal();
135 | export const market = () => {
136 | const s = marketSubscription();
137 | const ms = s?.data?.markets ?? [];
138 | const _ms = ms.map((m) => calcAttributes(m));
139 | return _ms.at(0);
140 | };
141 | export const [historyOptionSubscription, setHistoryOptionSubscription] =
142 | createSignal();
143 |
144 | const historyOptions = () =>
145 | historyOptionSubscription()?.data?.history__options ?? [];
146 |
147 | export const chartSeries = () => {
148 | const m = market();
149 | if (!m) return [];
150 |
151 | const result: {
152 | [option_id: string]: Series;
153 | } = {};
154 |
155 | m.options.forEach((o) => {
156 | result[o.id] = {
157 | data: [],
158 | id: o.id,
159 | options: {
160 | color: o.color,
161 | lineType: LineType.WithSteps,
162 | lineWidth: 2,
163 | lastPriceAnimation: LastPriceAnimationMode.Continuous,
164 | },
165 | title: o.name,
166 | };
167 | });
168 |
169 | historyOptions().forEach((h) => {
170 | if (!result[h.option_id]) return;
171 | result[h.option_id].data.push({
172 | time: Math.floor(new Date(h.created_at).getTime() / 1000) as UTCTimestamp,
173 | value: h.yesProb,
174 | });
175 | });
176 |
177 | return Object.values(result).map((s) => {
178 | const sorted = s.data.toSorted((a, b) => a.time - b.time);
179 | return {
180 | ...s,
181 | data: sorted,
182 | };
183 | });
184 | };
185 |
186 | export const [scrolledToBottom, setScrolledToBottom] = createSignal();
187 | export const [abortController, setAbortController] = createSignal<
188 | AbortController | undefined
189 | >();
190 |
191 | // export const [blocksScrollView, setBlocksScrollView] =
192 | // createSignal();
193 |
194 | export const userChatted = () => listBlocks().length > 0;
195 | export const [news, setNews] = createSignal<{ title: string }[]>([
196 | {
197 | title: "Government announces new tax policies for 2023",
198 | },
199 | {
200 | title: "Tesla announces new model 3",
201 | },
202 | {
203 | title: "Scientists discover new species of cat that can play the piano",
204 | },
205 | {
206 | title: "SpaceX launches new rocket to Mars",
207 | },
208 | {
209 | title: "Apple unveils its latest iPhone model",
210 | },
211 | {
212 | title: "Breakthrough in cancer research offers new hope",
213 | },
214 | {
215 | title: "Global markets react to unexpected interest rate hike",
216 | },
217 | {
218 | title: "Major tech company faces data breach allegations",
219 | },
220 |
221 | {
222 | title: "Major tech company faces data breach allegations",
223 | },
224 |
225 | {
226 | title: "Major tech company faces data breach allegations",
227 | },
228 |
229 | {
230 | title: "Major tech company faces data breach allegations",
231 | },
232 | ]);
233 |
234 | export const [bigLogoEl, setBigLogoEl] = createSignal();
235 |
236 | export const [infiniteScrollHovered, setInfiniteScrollHovered] =
237 | createSignal(false);
238 |
239 | export const scrollToEnd = () => {
240 | // console.log("scrolling", document.body.scrollHeight);
241 | window.scrollTo({
242 | // 80 is random for no reason
243 | top: document.body.scrollHeight + 80,
244 | behavior: "smooth",
245 | });
246 | };
247 |
248 | export const [toolTmpStorage, setToolTmpStorage] = createSignal({});
249 |
250 | export async function api_vote(market_id: string, vote: UpvoteDownvote) {
251 | console.log("call vote", market_id);
252 | const resp = await fetch(`/api/markets/${market_id}/vote`, {
253 | method: "POST",
254 | headers: {
255 | "Content-Type": "application/json",
256 | },
257 | body: JSON.stringify(vote),
258 | });
259 |
260 | console.log("resp", resp, resp.ok);
261 | console.log("j", await resp.text());
262 |
263 | return resp;
264 | }
265 |
266 | export const [blockShow, setBlockShow] = createSignal<{
267 | [blockId: string]: boolean;
268 | }>({});
269 |
--------------------------------------------------------------------------------
/src/components/AIComp.tsx:
--------------------------------------------------------------------------------
1 | import { id } from "@instantdb/core";
2 |
3 | import { BsArrowUpShort, BsStopFill } from "solid-icons/bs";
4 | import { createSignal, For, onMount, Show } from "solid-js";
5 | import {
6 | abortController,
7 | blocks,
8 | blocksToList,
9 | listBlocks,
10 | profile,
11 | scrollToEnd,
12 | setAbortController,
13 | setBigLogoEl,
14 | setBlocks,
15 | userChatted,
16 | } from "~/client/utils";
17 |
18 | import { accept } from "~/client/accept";
19 | import { db } from "~/client/database";
20 | import BlockComp from "./BlockComp";
21 | import IconComp from "./IconComp";
22 | import Spinner from "./Spinner";
23 | import { generateBlocks, readNDJSON } from "~/shared/utils";
24 |
25 | export default function AIComp() {
26 | const [text, setText] = createSignal("");
27 |
28 | async function submit() {
29 | const ac = abortController();
30 | if (ac) {
31 | ac.abort();
32 | }
33 |
34 | const t = text().trim();
35 | if (!t) return;
36 |
37 | scrollToEnd();
38 | setText("");
39 |
40 | const userBlock: Block = {
41 | agent_step: undefined,
42 | id: id(),
43 | role: "user",
44 | content: t,
45 | created_at: new Date().toISOString(),
46 | updated_at: new Date().toISOString(),
47 | };
48 |
49 | // Log the user's message
50 | db.transact([
51 | db.tx.blocks[userBlock.id].update(userBlock).link({
52 | profile: profile()?.id,
53 | }),
54 | ]);
55 |
56 | const blocks_1 = {
57 | ...blocks(),
58 | [userBlock.id]: userBlock,
59 | };
60 | setBlocks(blocks_1);
61 |
62 | const onlyAssistantOrUser = blocksToList(blocks_1).filter(
63 | (b) => b.role == "user" || b.role == "assistant"
64 | );
65 |
66 | let history = [];
67 | let remainingLength = 1000;
68 |
69 | for (let i = onlyAssistantOrUser.length - 1; i >= 0; i--) {
70 | const { content } = onlyAssistantOrUser[i];
71 |
72 | if (content.length > remainingLength) {
73 | // Add only the part of the content that fits
74 | history.push({
75 | ...onlyAssistantOrUser[i],
76 | content: content.slice(0, remainingLength),
77 | });
78 | break;
79 | }
80 |
81 | // Add the full content if it fits
82 | history.push(onlyAssistantOrUser[i]);
83 | remainingLength -= content.length;
84 | }
85 |
86 | history = history.toReversed();
87 |
88 | const controller = new AbortController();
89 | const { signal } = controller;
90 | signal.addEventListener("abort", () => {
91 | console.log("aborted!");
92 | });
93 | setAbortController(controller);
94 |
95 | const resp = await fetch("/api/complete", {
96 | method: "POST",
97 | headers: {
98 | "Content-Type": "application/json",
99 | },
100 | body: JSON.stringify({ blocks: history } as APICompleteBody),
101 | signal,
102 | });
103 |
104 | if (!resp.body) return;
105 | const g = readNDJSON(resp.body);
106 |
107 | for await (const value of g) {
108 | const y = value as any as ChatStreamYield;
109 | accept(y);
110 | }
111 |
112 | setAbortController(undefined);
113 | }
114 |
115 | // onMount(() => {
116 | // setBlocks(generateBlocks());
117 | // });
118 |
119 | return (
120 |
121 |
122 |
123 |
127 |
128 |
129 |
130 |
131 |
132 | Predict Anything
133 |
134 |
135 |
136 |
137 |
138 | We will tell how accurate your prediction is.
139 | {" "}
140 |
141 | If no data is available, a prediction market will be
142 | created to gather insights from the crowd.
143 |
144 |
145 |
146 |
147 |
148 | }
149 | >
150 |
151 |
152 | {(b) => }
153 |
154 | {/*
155 |
156 | */}
157 |
158 |
159 |
160 |
161 |
162 |
166 |
185 | }
186 | >
187 |
188 |
189 |
190 |
Researching
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 | );
200 | }
201 |
--------------------------------------------------------------------------------
/src/components/BlockComp.tsx:
--------------------------------------------------------------------------------
1 | import { marked } from "marked";
2 | import { BsChevronDown, BsChevronUp, BsTerminal } from "solid-icons/bs";
3 | import { For, Show } from "solid-js";
4 | import { Dynamic } from "solid-js/web";
5 | import { blockShow, listBlocks, setBlockShow } from "~/client/utils";
6 | import IconComp from "./IconComp";
7 |
8 | import {
9 | CreateMarketToolArgs,
10 | CreateMarketToolDone,
11 | } from "~/shared/tools/createMarket";
12 | import {
13 | SearchImagesToolArgs,
14 | SearchImagesToolDoing,
15 | SearchImagesToolDone,
16 | } from "~/shared/tools/searchImages";
17 | import { SearchWebToolArgs, SearchWebToolDone } from "~/shared/tools/searchWeb";
18 | import { ToolName } from "~/shared/tools/utils";
19 | import MarketCard from "./MarketCard";
20 | import MarketImage from "./MarketImage";
21 |
22 | function AssistantBlockComp(props: { block: AssistantBlock }) {
23 | let ref!: HTMLDivElement;
24 |
25 | // TODO: sanitize
26 | const html = () => marked.parse(props.block.content) as string;
27 |
28 | return (
29 |
35 | );
36 | }
37 |
38 | function AssistantReasoningForwardBlockComp(props: { block: AssistantBlock }) {
39 | let ref!: HTMLDivElement;
40 |
41 | // TODO: sanitize
42 | const html = () => marked.parse(props.block.content) as string;
43 |
44 | return (
45 |
49 |
51 | setBlockShow((p) => ({
52 | ...p,
53 | [props.block.id]: !p[props.block.id],
54 | }))
55 | }
56 | class="flex items-center space-x-2"
57 | >
58 | PLANNING
59 |
60 | }
63 | >
64 |
65 |
66 |
67 |
68 |
69 |
70 |
74 |
75 |
76 | );
77 | }
78 |
79 | function ReasoningBlockComp(props: { block: ReasoningBlock }) {
80 | let ref!: HTMLDivElement;
81 |
82 | return (
83 |
87 |
89 | setBlockShow((p) => ({
90 | ...p,
91 | [props.block.id]: !p[props.block.id],
92 | }))
93 | }
94 | class="flex items-center space-x-2"
95 | >
96 | THINKING
97 |
98 | }
101 | >
102 |
103 |
104 |
105 |
106 |
107 |
108 | {props.block.content}
109 |
110 |
111 | );
112 | }
113 |
114 | function UserBlockComp(props: { block: UserBlock }) {
115 | return (
116 |
117 |
118 |
119 |
{props.block.content}
120 |
121 |
122 | );
123 | }
124 |
125 | function ToolBlockBody_ArgumentsString(props: { block: ToolBlock }) {
126 | return (
127 |
128 |
129 |
130 |
{props.block.content.arguments_partial_str}
131 |
132 |
133 | );
134 | }
135 |
136 | function ToolBlockBody_ResultError(props: { block: ToolBlock }) {
137 | return (
138 |
139 |
140 |
141 |
{JSON.stringify(props.block.content.result)}
142 |
143 |
144 | );
145 | }
146 |
147 | function ToolBlockBody_createMarket(props: {
148 | block: ToolBlock;
149 | }) {
150 | return (
151 | }
154 | >
155 | {(result) => (
156 |
157 | }
160 | >
161 | {(market_id) => }
162 |
163 |
164 | )}
165 |
166 | );
167 | }
168 |
169 | function ToolBlockBody_searchWeb(props: {
170 | block: ToolBlock;
171 | }) {
172 | const favicon = (url: string) =>
173 | `http://www.google.com/s2/favicons?domain=${url}&sz=128`;
174 | return (
175 | }
178 | >
179 |
180 |
181 |
182 |
183 | Searched for "{props.block.content.arguments?.query}"
184 |
185 |
186 |
187 | {(result) => (
188 |
192 |
193 | {(site) => (
194 |
195 | )}
196 |
197 |
198 | }
199 | >
200 |
No results found.
201 |
202 | )}
203 |
204 |
205 |
206 |
207 | );
208 | }
209 |
210 | function ToolBlockBody_searchImages(props: {
211 | block: ToolBlock<
212 | SearchImagesToolArgs,
213 | SearchImagesToolDone,
214 | SearchImagesToolDoing
215 | >;
216 | }) {
217 | const data = () => props.block.content.doings.at(0)?.data;
218 | return (
219 | }
222 | >
223 |
224 |
225 |
226 |
227 | Searched for "{props.block.content.arguments?.query}"
228 |
229 |
230 |
231 | {(d) => (
232 |
233 |
234 | {(r) => (
235 |
240 | )}
241 |
242 |
243 | )}
244 |
245 |
246 |
247 |
248 | );
249 | }
250 |
251 | function ToolBlockComp(props: { block: ToolBlock }) {
252 | const body = {
253 | [ToolName.createMarket as string]: ToolBlockBody_createMarket,
254 | [ToolName.searchWeb as string]: ToolBlockBody_searchWeb,
255 | [ToolName.searchImages as string]: ToolBlockBody_searchImages,
256 | }[props.block.content.name];
257 |
258 | const doing = () => props.block.content.result === undefined;
259 | return (
260 |
261 |
{
263 | setBlockShow((p) => ({
264 | ...p,
265 | [props.block.id]: !p[props.block.id],
266 | }));
267 | }}
268 | class="px-4 py-2 bg-neutral-900 hover:bg-neutral-800 rounded border border-neutral-800 flex items-center space-x-2"
269 | >
270 |
271 |
272 |
273 |
274 | {props.block.content.name}
275 |
276 |
277 | }
280 | >
281 |
282 |
283 |
284 |
285 |
286 |
287 | {/* {JSON.stringify(props.block)}
*/}
288 |
289 |
290 | );
291 | }
292 |
293 | function AssistantLabel() {
294 | return (
295 |
296 |
297 |
298 |
299 | );
300 | }
301 |
302 | function sectionType(role: Block["role"]) {
303 | if (role == "user") return 1;
304 | return 0;
305 | }
306 |
307 | export default function BlockComp(props: { blockId: string }) {
308 | const block = (): UIBlock => {
309 | const l = listBlocks();
310 | const i = l.findIndex((b) => b.id === props.blockId);
311 | const previousBlock = l.at(i - 1);
312 | const nextBlock = l.at(i + 1);
313 | return {
314 | ...l[i],
315 | isStartSecion: previousBlock
316 | ? sectionType(previousBlock.role) !== sectionType(l[i].role)
317 | : true,
318 | isEndSecion: nextBlock
319 | ? sectionType(nextBlock.role) !== sectionType(l[i].role)
320 | : true,
321 | };
322 | };
323 |
324 | const assistantComponents: any = {
325 | reasoning_and_foward: AssistantReasoningForwardBlockComp,
326 | tool_call_and_content: AssistantBlockComp,
327 | };
328 |
329 | const components = {
330 | assistant: (props: { block: AssistantBlock }) => (
331 |
332 | {(step) => (
333 |
337 | )}
338 |
339 | ),
340 |
341 | user: UserBlockComp,
342 | tool: ToolBlockComp,
343 | reasoning: ReasoningBlockComp,
344 | };
345 |
346 | const label = ;
347 |
348 | return (
349 |
350 | {(b) => (
351 |
352 |
353 | {label}
354 |
355 |
356 |
357 |
358 | )}
359 |
360 | );
361 | }
362 |
--------------------------------------------------------------------------------
/src/components/BuyComp.tsx:
--------------------------------------------------------------------------------
1 | import { BsCheckCircleFill, BsXCircleFill } from "solid-icons/bs";
2 | import { createEffect, createSignal, Show } from "solid-js";
3 | import { optionId, setType, type } from "~/client/utils";
4 | import { buyShare, MIN_USD_AMOUNT, noProb } from "~/shared/utils";
5 | import CheckBoxItem from "./CheckboxItem";
6 | import Spinner from "./Spinner";
7 | import Header from "./buysell/Header";
8 |
9 | export default function BuyComp(props: BuySellProps) {
10 | const option = () => props.market.options.find((o) => o.id == optionId());
11 | const [amount, setAmount] = createSignal();
12 | const [numShareBuy, setNumShareBuy] = createSignal();
13 | const [avgPrice, setAvgPrice] = createSignal();
14 | const [status, setstatus] = createSignal<
15 | Status
16 | >({
17 | idle: {},
18 | });
19 | const [payout, setPayout] = createSignal<{
20 | total: number;
21 | earn: number;
22 | }>();
23 | const [error, setError] = createSignal<{
24 | message: string;
25 | }>();
26 | const [highlightAmountError, setHighlightAmountError] = createSignal(false);
27 |
28 | function doPurchase(type: YesOrNo, usdAmount?: number) {
29 | const o = option();
30 | if (!o) return undefined;
31 | const shareId = o.shares.find((s) => s.type == type)?.id;
32 | if (!shareId) return undefined;
33 | const result = buyShare(o.shares, shareId, usdAmount);
34 | return result;
35 | }
36 |
37 | createEffect(() => {
38 | const a = amount() ?? 0;
39 | const m = type();
40 | const ac = Math.max(a, MIN_USD_AMOUNT);
41 | const purchase = doPurchase(m, ac);
42 | setAvgPrice(purchase?.avgPrice);
43 | const nsb = a < MIN_USD_AMOUNT ? 0 : purchase?.shareOut;
44 | setNumShareBuy(nsb);
45 | setPayout(
46 | nsb
47 | ? {
48 | total: nsb * 1,
49 | earn: nsb * 1 - ac,
50 | }
51 | : undefined
52 | );
53 |
54 | setError();
55 | // The AMM cannot handle
56 | if (!purchase) {
57 | setError({
58 | message:
59 | "The market cannot handle this much, try buying a smaller amount",
60 | });
61 | }
62 | });
63 |
64 | const buy = async () => {
65 | const a = amount();
66 | const o = option();
67 | const m = type();
68 |
69 | if (!o) {
70 | console.warn("no option");
71 | return;
72 | }
73 |
74 | console.log("purchase...", a);
75 | if (!a || a < MIN_USD_AMOUNT) {
76 | setHighlightAmountError(true);
77 | return;
78 | }
79 | setHighlightAmountError(false);
80 |
81 | const share = o.shares.find((s) => s.type == m);
82 | if (!share) {
83 | console.error("no such share");
84 | return;
85 | }
86 | const shareId = share.id;
87 |
88 | console.log("shareId", shareId, o.shares, m);
89 |
90 | try {
91 | const action: BuySellAction = {
92 | type: "buy",
93 | amount: a,
94 | shareId,
95 | };
96 |
97 | setstatus({
98 | doing: {},
99 | });
100 | console.log("send", action, o.id);
101 | const response = await fetch(`/api/options/${o.id}/action`, {
102 | method: "POST",
103 | headers: {
104 | "Content-Type": "application/json",
105 | },
106 | body: JSON.stringify(action),
107 | });
108 | if (!response.ok) {
109 | const msg = await response.text();
110 | throw new Error(`Error: ${msg}`);
111 | }
112 |
113 | const data =
114 | (await response.json()) as NonNullable;
115 | setstatus({
116 | done_succ: {
117 | ...data,
118 | type: m,
119 | },
120 | });
121 |
122 | // clear input
123 | setAmount();
124 | } catch (e) {
125 | console.error("fetch error", e);
126 | setstatus({
127 | done_err: {},
128 | });
129 | return;
130 | }
131 | };
132 |
133 | return (
134 |
135 |
136 | {(succ) => (
137 |
138 |
139 |
Transaction success!
140 |
141 | You have bought {succ().shareOut}{" "}
142 | {succ().type == "yes" ? "Yes" : "No"} shares at an average price
143 | of ${succ().avgPrice.toFixed(2)}
144 |
145 |
146 | )}
147 |
148 |
149 |
150 | {(err) => (
151 |
152 |
153 |
Transaction failed!
154 |
155 | Your transaction could not be processed. Please try again later or
156 | contact support for assistance.
157 |
158 |
159 | )}
160 |
161 |
162 |
163 |
164 |
169 |
170 |
171 |
174 | Select an option to buy
175 |
176 | }
177 | when={option()}
178 | >
179 | {(o) => (
180 |
181 |
182 |
183 |
184 |
192 |
200 |
201 |
202 |
203 |
207 | Amount must be more than ${MIN_USD_AMOUNT}
208 |
209 |
210 |
Amount $
211 |
{
216 | setAmount(
217 | Number.isNaN(e.currentTarget.valueAsNumber)
218 | ? undefined
219 | : e.currentTarget.valueAsNumber
220 | );
221 | }}
222 | type="number"
223 | class="bg-transparent outline-none text-right max-w-none flex-1 p-4 placeholder:text-neutral-500 min-w-0"
224 | placeholder="0"
225 | />
226 |
227 |
228 |
229 |
230 |
234 |
235 |
236 | Number of Shares to Acquire
237 |
238 |
{numShareBuy()?.toFixed(0) ?? "N/A"}
239 |
240 |
241 |
Average Price
242 |
${avgPrice()?.toFixed(2) ?? "N/A"}
243 |
244 |
245 | {(p) => (
246 |
247 |
248 | Payout if{" "}
249 | {o().name} {" "}
250 | wins
251 |
252 |
253 |
254 | ${p().total.toFixed(2)}{" "}
255 |
256 | (+${p().earn.toFixed(2)})
257 |
258 |
259 |
260 | )}
261 |
262 | >
263 | }
264 | >
265 | {(e) => {e().message}
}
266 |
267 |
268 |
269 |
270 | {
272 | buy();
273 | }}
274 | data-type={type()}
275 | class="w-full
276 | data-[type=yes]:bg-blue-500
277 | data-[type=yes]:hover:bg-blue-700
278 | data-[type=no]:bg-red-500
279 | data-[type=no]:hover:bg-red-700
280 | transition-all
281 | text-white p-4 rounded font-bold"
282 | >
283 | Buy {type() === "yes" ? "Yes" : "No"}
284 |
285 |
286 |
287 | )}
288 |
289 |
290 |
291 | );
292 | }
293 |
--------------------------------------------------------------------------------
/src/components/BuySellComp.tsx:
--------------------------------------------------------------------------------
1 | import BuyComp from "./BuyComp";
2 | import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js";
3 | import { optionId, profile, setOptionId, setType } from "~/client/utils";
4 | import SellComp from "./SellComp";
5 | import { useSearchParams } from "@solidjs/router";
6 | import { db } from "~/client/database";
7 |
8 | export default function BuySellComp(props: BuySellProps) {
9 | const option = () => props.market.options.find((o) => o.id == optionId());
10 | const [mode, setMode] = createSignal<"buy" | "sell">("buy");
11 | const [holdingSubscription, setHoldingSubscription] =
12 | createSignal();
13 | const numShares = () => {
14 | const s = holdingSubscription();
15 |
16 | if (!s || !s.data) return {};
17 | let result: { [type: string]: number } = {};
18 | s.data.holdings.forEach((h) => {
19 | const type = h.share?.type;
20 | if (!type) return;
21 | result[type] = h.amount;
22 | });
23 |
24 | return result;
25 | };
26 |
27 | const [searchParams, setSearchParams] = useSearchParams();
28 | createEffect(() => {
29 | if (searchParams.optionId) {
30 | setOptionId(searchParams.optionId as string);
31 | return;
32 | }
33 | console.log("props.market.options", props.market.options);
34 | const firstOption = props.market.options.at(0);
35 | if (firstOption) {
36 | setOptionId(firstOption.id);
37 | return;
38 | }
39 |
40 | setOptionId();
41 | });
42 |
43 | onMount(() => {
44 | if (searchParams.shareId) {
45 | const share = option()?.shares.find((s) => s.id == searchParams.shareId);
46 | if (share) {
47 | if (["yes", "no"].includes(share.type)) {
48 | setType(share.type as YesOrNo);
49 | }
50 | }
51 | return;
52 | }
53 | });
54 |
55 | createEffect(() => {
56 | const p = profile();
57 | const o = option();
58 | if (!p || !o) return;
59 | const shareIds = o.shares.map((s) => s.id);
60 |
61 | const unsub = db.subscribeQuery(
62 | {
63 | holdings: {
64 | $: {
65 | where: {
66 | "profile.id": p.id,
67 | "share.id": {
68 | $in: shareIds,
69 | },
70 | },
71 | },
72 | share: {},
73 | },
74 | },
75 | (resp) => {
76 | console.log("resp holdings for this option", resp);
77 | setHoldingSubscription(resp);
78 | }
79 | );
80 | onCleanup(() => {
81 | unsub();
82 | });
83 | });
84 |
85 | return (
86 |
87 |
91 |
92 |
93 | }
94 | >
95 |
96 |
97 |
98 |
99 |
100 |
101 | You are having {(numShares().yes || 0).toFixed(2)} Yes shares and{" "}
102 | {(numShares().no || 0).toFixed(2)} No shares.
103 |
104 | Click{" "}
105 | setMode((m) => (m == "buy" ? "sell" : "buy"))}
107 | class="underline decoration-dashed hover:decoration-solid"
108 | >
109 | here
110 | {" "}
111 | to {mode() == "buy" ? "sell" : "buy"}.
112 |
113 |
114 | );
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/CheckBox.tsx:
--------------------------------------------------------------------------------
1 | import { BsCheck } from "solid-icons/bs";
2 | import { Show } from "solid-js";
3 |
4 | export default function CheckBox(props: {
5 | signal?: T;
6 | value?: T;
7 | onChange?: (value?: T) => void;
8 | checked?: boolean;
9 | }) {
10 | const checked = () => props.checked ?? props.value == props.signal;
11 |
12 | return (
13 | {
15 | props.onChange?.(props.value);
16 | }}
17 | class="cursor-pointer relative border border-neutral-800 rounded p-1 w-6 h-6 group-hover:border-neutral-500 transition-all hover:bg-white/10"
18 | >
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/CheckboxItem.tsx:
--------------------------------------------------------------------------------
1 | import { Show } from "solid-js";
2 | import { probToPercent } from "~/shared/utils";
3 | import CheckBox from "./CheckBox";
4 |
5 | export default function CheckBoxItem(props: {
6 | label: string;
7 | price?: number;
8 | prob: number | undefined;
9 | id: YesOrNo;
10 | value?: YesOrNo;
11 | hideCheckBox?: boolean;
12 | onChange?: (value: YesOrNo) => void;
13 | }) {
14 | const checked = () => props.value === props.id;
15 | const colorClass = () =>
16 | ({
17 | yes: `bg-blue-500/20 group-hover:bg-blue-500/40 data-[checked=true]:bg-blue-500/40`,
18 | no: `bg-red-500/20 group-hover:bg-red-500/40 data-[checked=true]:bg-red-500/40`,
19 | }[props.id]);
20 |
21 | return (
22 | {
24 | props.onChange?.(props.id);
25 | }}
26 | class="group relative p-4 flex items-center space-x-2 border rounded border-neutral-800 bg-white/5 flex-1 cursor-pointer "
27 | >
28 |
35 |
36 |
{props.label}
37 |
{props.price ? `$${props.price.toFixed(2)}` : "N/A"}
41 | }
42 | >
43 |
44 | {probToPercent(props.prob)}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/IconComp.tsx:
--------------------------------------------------------------------------------
1 | export default function IconComp(props: {
2 | size?: "sm" | "md" | "lg" | "xs" | "tiny";
3 | inline?: boolean;
4 | }) {
5 | const sizeCls = {
6 | tiny: "w-2 h-2",
7 | xs: "w-4 h-4",
8 | sm: "w-8 h-8",
9 | md: "w-10 h-10",
10 | lg: "w-12 h-12",
11 | }[props.size ?? "md"];
12 |
13 | return (
14 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Image.tsx:
--------------------------------------------------------------------------------
1 | import { createSignal, onMount, Show } from "solid-js";
2 |
3 | export default function MaybeImage(props: {
4 | src: string;
5 | alt?: string;
6 | class: string;
7 | }) {
8 | const [loaded, setLoaded] = createSignal(false);
9 | onMount(() => {
10 | const img = new Image();
11 | img.src = props.src;
12 | img.onload = () => setLoaded(true);
13 | });
14 | return (
15 | }
18 | >
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/InfiniteScroll.tsx:
--------------------------------------------------------------------------------
1 | import { setInfiniteScrollHovered } from "~/client/utils";
2 | import TranslatingGroup from "./TranslatingGroup";
3 |
4 | export default function InfiniteScroll() {
5 | return (
6 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/MarketCard.tsx:
--------------------------------------------------------------------------------
1 | import { createEffect, createSignal, For, onMount, Show } from "solid-js";
2 |
3 | import {
4 | BiRegularDownvote,
5 | BiRegularUpvote,
6 | BiSolidDownvote,
7 | BiSolidUpvote,
8 | } from "solid-icons/bi";
9 | import { db } from "~/client/database";
10 | import { api_vote, markets, profile } from "~/client/utils";
11 | import { calcAttributes, Color, noProb, numF, prob } from "~/shared/utils";
12 | import CheckBoxItem from "./CheckboxItem";
13 | import MarketImage from "./MarketImage";
14 | import OptionItem from "./OptionItem";
15 | import MarketSocialComp from "./MarketSocialComp";
16 |
17 | export default function MarketCard(props: {
18 | marketId: string;
19 | queryAgain?: boolean;
20 | }) {
21 | const [marketResponse, setMarketResponse] = createSignal();
22 | const m = () => {
23 | const m = marketResponse()?.data.markets.at(0);
24 | if (!m) return;
25 | return calcAttributes(m);
26 | };
27 | const m0 = () => markets().find((m) => m?.id == props.marketId);
28 |
29 | const market = () => m() ?? m0();
30 |
31 | onMount(async () => {
32 | if (!props.queryAgain) return;
33 | const resp = await db.queryOnce({
34 | markets: {
35 | options: {
36 | shares: {},
37 | },
38 | $: {
39 | where: {
40 | id: props.marketId,
41 | },
42 | },
43 | },
44 | });
45 | setMarketResponse(resp);
46 | });
47 |
48 | const redirectToMarket = (optionId?: string, shareId?: string) => {
49 | const url = new URL(`/market/${props.marketId}`, window.location.origin);
50 | if (optionId) url.searchParams.set("optionId", optionId);
51 | if (shareId) url.searchParams.set("shareId", shareId);
52 | // Push the new state to the browser history
53 | window.history.pushState({}, "", url.toString());
54 | window.location.href = url.toString();
55 | };
56 |
57 | return (
58 |
59 | {(m) => (
60 |
61 |
155 |
156 | )}
157 |
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/src/components/MarketImage.tsx:
--------------------------------------------------------------------------------
1 | import MaybeImage from "./Image";
2 |
3 | export default function MarketImage(props: {
4 | src: string;
5 | size?: "sm" | "md";
6 | }) {
7 | const sizeClass = {
8 | sm: "w-12 h-12",
9 | md: "w-24 h-24",
10 | }[props.size ?? "md"];
11 | return (
12 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/MarketSocialComp.tsx:
--------------------------------------------------------------------------------
1 | import { createEffect, createSignal, Show } from "solid-js";
2 |
3 | import {
4 | BiRegularDownvote,
5 | BiRegularUpvote,
6 | BiSolidDownvote,
7 | BiSolidUpvote,
8 | } from "solid-icons/bi";
9 | import { db } from "~/client/database";
10 | import { api_vote, profile } from "~/client/utils";
11 | import { numF } from "~/shared/utils";
12 | import { InstantCoreDatabase } from "@instantdb/core";
13 | import { AppSchema } from "../../instant.schema";
14 |
15 | export default function MarketSocialComp(props: { marketId: string }) {
16 | const [marketResponse, setMarketResponse] =
17 | createSignal();
18 | const market = () => marketResponse()?.data.markets.at(0);
19 | const [dbvote, setDBVote] = createSignal<1 | 0 | -1>(0);
20 | const [vote, setVote] = createSignal<1 | 0 | -1>(0);
21 |
22 | const num_votes = () => {
23 | const n = market()?.num_votes ?? 0;
24 | const diff = dbvote() - vote();
25 | return numF(n - diff);
26 | };
27 |
28 | // We need to check if the user has liked this post before or not
29 | createEffect(async () => {
30 | const p = profile();
31 | if (!p) return;
32 | const resp = await db.queryOnce({
33 | markets: {
34 | $: {
35 | where: {
36 | id: props.marketId,
37 | },
38 | },
39 | votes: {
40 | $: {
41 | where: {
42 | profile: p.id,
43 | },
44 | },
45 | },
46 | },
47 | });
48 |
49 | setMarketResponse(resp);
50 | const vote = resp.data.markets.at(0)?.votes.at(0);
51 | // At the start, theey are all the same
52 | if (vote) {
53 | setDBVote(vote.isUpvote ? 1 : -1);
54 | setVote(vote.isUpvote ? 1 : -1);
55 | } else {
56 | setDBVote(0);
57 | setVote(0);
58 | }
59 | });
60 |
61 | return (
62 |
63 | {(m) => (
64 |
65 |
66 |
67 |
{
69 | console.log("vote", vote());
70 | if (vote() == 1) {
71 | setVote(0);
72 | api_vote(m().id, {
73 | type: "remove",
74 | });
75 | return;
76 | }
77 |
78 | setVote(1);
79 | api_vote(m().id, {
80 | type: "upvote",
81 | });
82 | }}
83 | >
84 |
88 | }
91 | >
92 |
93 |
94 |
95 |
96 |
97 |
100 |
101 |
{
103 | if (vote() == -1) {
104 | setVote(0);
105 | api_vote(m().id, {
106 | type: "remove",
107 | });
108 | return;
109 | }
110 |
111 | setVote(-1);
112 | api_vote(m().id, {
113 | type: "downvote",
114 | });
115 | }}
116 | >
117 |
121 | }
124 | >
125 |
126 |
127 |
128 |
129 |
130 |
131 | )}
132 |
133 | );
134 | }
135 |
--------------------------------------------------------------------------------
/src/components/Markets.tsx:
--------------------------------------------------------------------------------
1 | import { TbLoader } from "solid-icons/tb";
2 | import { createEffect, For, onMount, Show } from "solid-js";
3 | import {
4 | loadMarkets,
5 | loadMarketsState,
6 | markets,
7 | marketsHasNextPage,
8 | profile,
9 | } from "~/client/utils";
10 | import Spinner from "./Spinner";
11 | import MarketCard from "./MarketCard";
12 |
13 | export default function Markets() {
14 | onMount(async () => {
15 | loadMarkets();
16 | });
17 |
18 | return (
19 |
20 |
21 | {(m) => }
22 |
23 |
24 |
25 |
26 | {
29 | loadMarkets();
30 | }}
31 | >
32 | Load More
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/MartketChart.tsx:
--------------------------------------------------------------------------------
1 | import { createChart, CrosshairMode, IChartApi } from "lightweight-charts";
2 | import { BsChevronBarRight } from "solid-icons/bs";
3 | import {
4 | createEffect,
5 | createSignal,
6 | For,
7 | onCleanup,
8 | onMount,
9 | untrack,
10 | } from "solid-js";
11 | import { chartSeries } from "~/client/utils";
12 | import { lastItemToUSD } from "~/shared/utils";
13 |
14 | export default function MarketChart() {
15 | const [chart, setChart] = createSignal();
16 | const [lineSeries, setLineSeries] = createSignal<{
17 | [id: string]: ReturnType | undefined;
18 | }>({});
19 | let container!: HTMLDivElement;
20 | onMount(() => {
21 | // Create the Lightweight Chart within the container element
22 | const chart = createChart(container, {
23 | localization: {
24 | timeFormatter: (timestamp: number) => {
25 | return new Date(timestamp * 1000).toLocaleString();
26 | },
27 | },
28 | layout: {
29 | background: { color: "transparent" },
30 | textColor: "#737373",
31 | attributionLogo: false,
32 | },
33 | grid: {
34 | vertLines: { color: "transparent" },
35 | horzLines: { color: "transparent" },
36 | },
37 | rightPriceScale: {
38 | borderVisible: false,
39 | textColor: "#737373",
40 | },
41 | timeScale: {
42 | timeVisible: true,
43 | secondsVisible: true,
44 | borderVisible: false,
45 | },
46 | crosshair: {
47 | mode: CrosshairMode.Normal,
48 | vertLine: {
49 | labelBackgroundColor: "black",
50 | },
51 | horzLine: {
52 | labelBackgroundColor: "black",
53 | },
54 | },
55 | });
56 | setChart(chart);
57 |
58 | // Resize the chart when the window is resized
59 | const resize = () => {
60 | const { width, height } = container.getBoundingClientRect();
61 | chart.resize(width, height);
62 | };
63 | window.addEventListener("resize", resize);
64 | onCleanup(() => {
65 | window.removeEventListener("resize", resize);
66 | });
67 |
68 | const canvas = container.querySelector("canvas");
69 | if (canvas) {
70 | const parent = canvas.parentElement;
71 | if (parent) {
72 | parent.style.position = "relative";
73 | const dotsBg = document.createElement("div");
74 | dotsBg.className = "dots-bg";
75 | parent.appendChild(dotsBg);
76 | }
77 | }
78 | });
79 |
80 | const [tick, setTick] = createSignal(false);
81 | createEffect(() => {
82 | const t = setInterval(() => {
83 | setTick((prev) => !prev);
84 | }, 1000);
85 | onCleanup(() => {
86 | clearInterval(t);
87 | });
88 | });
89 |
90 | const chartSeriesRT = () => {
91 | const _ = tick();
92 | return chartSeries().map((s) => {
93 | const last_item = s.data.at(-1);
94 | if (!last_item) return s;
95 | const now = Math.floor(new Date().getTime() / 1000);
96 | const time = Math.max(now, last_item.time + 1);
97 | return {
98 | ...s,
99 | data: [
100 | ...s.data,
101 | {
102 | time,
103 | value: last_item.value,
104 | } as DataPoint,
105 | ],
106 | };
107 | });
108 | };
109 |
110 | createEffect(() => {
111 | const c = chart();
112 | if (!c) return;
113 | const _lineSeries = untrack(lineSeries);
114 | const cSeries = chartSeriesRT();
115 |
116 | let update = {};
117 | const _ = cSeries.forEach((cs) => {
118 | let s = _lineSeries[cs.id];
119 | if (!s) {
120 | s = c.addLineSeries(cs.options);
121 | update = {
122 | ...update,
123 | [cs.id]: s,
124 | };
125 | }
126 |
127 | s.setData(cs.data);
128 | });
129 |
130 | setLineSeries({
131 | ..._lineSeries,
132 | ...update,
133 | });
134 | });
135 |
136 | const onlyOneSeries = () => chartSeries().length == 1;
137 |
138 | return (
139 |
174 | );
175 | }
176 |
--------------------------------------------------------------------------------
/src/components/Nav.tsx:
--------------------------------------------------------------------------------
1 | import { useLocation } from "@solidjs/router";
2 | import {
3 | createEffect,
4 | createSignal,
5 | For,
6 | onCleanup,
7 | onMount,
8 | Show,
9 | } from "solid-js";
10 | import { bigLogoEl, profile } from "~/client/utils";
11 | import ProfileImage from "./ProfileImage";
12 | import { BsDiscord, BsGithub } from "solid-icons/bs";
13 | import IconComp from "./IconComp";
14 |
15 | export default function Nav() {
16 | const [showLogo, setShowLogo] = createSignal(false);
17 | const location = useLocation();
18 | const active = (path: string) =>
19 | path == location.pathname
20 | ? "border-white"
21 | : "border-transparent hover:border-white";
22 |
23 | createEffect(() => {
24 | const targetElement = bigLogoEl();
25 | if (!targetElement) return;
26 |
27 | // Define the callback function for the observer
28 | const observerCallback = (entries: any) => {
29 | entries.forEach((entry: any) => {
30 | if (entry.isIntersecting) {
31 | setShowLogo(false);
32 | } else {
33 | setShowLogo(true);
34 | }
35 | });
36 | };
37 |
38 | // Create an observer instance
39 | const observerOptions = {
40 | root: null, // Use the viewport as the container
41 | rootMargin: "0px", // Margin around the root
42 | threshold: 0.1, // Trigger when 10% of the element is visible
43 | };
44 |
45 | const observer = new IntersectionObserver(
46 | observerCallback,
47 | observerOptions
48 | );
49 |
50 | // Observe the target element
51 | observer.observe(targetElement);
52 |
53 | onCleanup(() => {
54 | observer.disconnect();
55 | });
56 | });
57 |
58 | return (
59 |
60 |
61 |
62 | {/*
63 |
64 | Predict
65 |
66 |
67 | Markets
68 |
69 | */}
70 |
71 |
{
73 | e.preventDefault();
74 | window.location.href = "/";
75 | }}
76 | >
77 |
78 |
79 |
80 |
81 |
82 |
88 |
89 |
90 |
91 |
97 |
98 |
99 |
100 |
101 |
102 | {(p) => (
103 | {
105 | window.location.href = `/profile/${p().id}`;
106 | }}
107 | class="flex-none flex items-center py-1 rounded px-2 space-x-4 text-white hover:bg-white/5 cursor-pointer border-transparent hover:border-neutral-800 border"
108 | >
109 |
110 |
{p().name}
111 |
112 | ${p().usd.toFixed(2)}
113 |
114 |
115 |
116 |
117 |
118 | )}
119 |
120 |
121 |
122 | {/*
*/}
163 |
164 | );
165 | }
166 |
--------------------------------------------------------------------------------
/src/components/NewsItem.tsx:
--------------------------------------------------------------------------------
1 | import { BsNewspaper } from "solid-icons/bs";
2 |
3 | export default function NewsItem(props: {
4 | n: { title: string; "aria-hidden"?: boolean };
5 | }) {
6 | return (
7 |
13 |
17 |
18 |
{props.n.title}
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/OptionImage.tsx:
--------------------------------------------------------------------------------
1 | export default function OptionImage() {
2 | return (
3 |
4 | );
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/OptionItem.tsx:
--------------------------------------------------------------------------------
1 | import { Color, colorClasses, probToPercent } from "~/shared/utils";
2 |
3 | export default function OptionItem(props: {
4 | id: string;
5 | label: string;
6 | prob?: number;
7 | color: Color;
8 | onClick?: (id: string) => void;
9 | }) {
10 | // const checked = () => props.value === props.id;
11 | const colorClass = () => colorClasses.background[props.color];
12 |
13 | return (
14 | {
16 | props.onClick?.(props.id);
17 | }}
18 | class="group relative p-2 flex items-center space-x-2 border rounded border-neutral-800 bg-white/5 flex-1 cursor-pointer h-12"
19 | >
20 |
27 |
28 |
29 |
{props.label}
30 |
{probToPercent(props.prob)}
31 |
32 |
33 | {/* hidden group-hover:block */}
34 | {/*
35 |
36 | Predict
37 |
38 |
*/}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/ProfileImage.tsx:
--------------------------------------------------------------------------------
1 | export default function ProfileImage(props: {
2 | size?:
3 | | "xs"
4 | | "sm"
5 | | "md"
6 | | "lg"
7 | | "xl"
8 | | "2xl"
9 | | "3xl"
10 | | "4xl"
11 | | "5xl"
12 | | "6xl";
13 | }) {
14 | const sizeClass = {
15 | xs: "h-4 w-4",
16 | sm: "h-6 w-6",
17 | md: "h-8 w-8",
18 | lg: "h-12 w-12",
19 | xl: "h-16 w-16",
20 | "2xl": "h-20 w-20",
21 | "3xl": "h-24 w-24",
22 | "4xl": "h-28 w-28",
23 | "5xl": "h-32 w-32",
24 | "6xl": "h-36 w-36",
25 | }[props.size ?? "md"];
26 |
27 | return (
28 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/SellComp.tsx:
--------------------------------------------------------------------------------
1 | import { BsCheckCircleFill, BsXCircleFill } from "solid-icons/bs";
2 | import { createEffect, createSignal, Show } from "solid-js";
3 | import { optionId, setType, type } from "~/client/utils";
4 | import { MIN_SHARE_AMOUNT, sellShare } from "~/shared/utils";
5 | import CheckBox from "./CheckBox";
6 | import OptionImage from "./OptionImage";
7 | import Spinner from "./Spinner";
8 | import Header from "./buysell/Header";
9 |
10 | export default function SellComp(props: BuySellProps) {
11 | const option = () => props.market.options.find((o) => o.id == optionId());
12 |
13 | const [amount, setAmount] = createSignal();
14 | const [numDollarTotal, setNumDollarTotal] = createSignal();
15 | const [avgPrice, setAvgPrice] = createSignal();
16 | const [status, setstatus] = createSignal<
17 | Status
18 | >({
19 | idle: {},
20 | });
21 | const [payout, setPayout] = createSignal<{
22 | total: number;
23 | earn: number;
24 | }>();
25 | const [error, setError] = createSignal<{
26 | message: string;
27 | }>();
28 | const [highlightAmountError, setHighlightAmountError] = createSignal(false);
29 |
30 | function doSale(type: YesOrNo, usdAmount?: number) {
31 | const o = option();
32 | if (!o) return undefined;
33 | const shareId = o.shares.find((s) => s.type == type)?.id;
34 | if (!shareId) return undefined;
35 | return sellShare(o.shares, shareId, usdAmount);
36 | }
37 |
38 | createEffect(() => {
39 | const a = amount() ?? 0;
40 | const m = type();
41 | const ac = Math.max(a, MIN_SHARE_AMOUNT);
42 | const sale = doSale(m, ac);
43 | setAvgPrice(sale?.avgPrice);
44 | const nusd = a < MIN_SHARE_AMOUNT ? 0 : sale?.usdOut;
45 | setNumDollarTotal(nusd);
46 | // setPayout(
47 | // nsb
48 | // ? {
49 | // total: nsb * 1,
50 | // earn: nsb * 1 - ac,
51 | // }
52 | // : undefined
53 | // );
54 |
55 | setError();
56 | // The AMM cannot handle
57 | if (!sale) {
58 | setError({
59 | message:
60 | "The market cannot handle this much, try selling a smaller amount",
61 | });
62 | }
63 | });
64 |
65 | const buy = async () => {
66 | const a = amount();
67 | const o = option();
68 | const m = type();
69 |
70 | if (!o) {
71 | console.warn("no option");
72 | return;
73 | }
74 |
75 | console.log("purchase...", a);
76 | if (!a || a < MIN_SHARE_AMOUNT) {
77 | setHighlightAmountError(true);
78 | return;
79 | }
80 | setHighlightAmountError(false);
81 |
82 | const share = o.shares.find((s) => s.type == m);
83 | if (!share) {
84 | console.error("no such share");
85 | return;
86 | }
87 | const shareId = share.id;
88 |
89 | console.log("shareId", shareId, o.shares, m);
90 |
91 | try {
92 | const action: BuySellAction = {
93 | type: "sell",
94 | amount: a,
95 | shareId,
96 | };
97 |
98 | setstatus({
99 | doing: {},
100 | });
101 | console.log("send", action, o.id);
102 | const response = await fetch(`/api/options/${o.id}/action`, {
103 | method: "POST",
104 | headers: {
105 | "Content-Type": "application/json",
106 | },
107 | body: JSON.stringify(action),
108 | });
109 | if (!response.ok) {
110 | const msg = await response.text();
111 | throw new Error(`Error: ${msg}`);
112 | }
113 |
114 | const data =
115 | (await response.json()) as NonNullable;
116 | setstatus({
117 | done_succ: {
118 | ...data,
119 | numShareSold: a,
120 | type: m,
121 | },
122 | });
123 |
124 | // clear input
125 | setAmount();
126 | } catch (e) {
127 | console.error("fetch error", e);
128 | setstatus({
129 | done_err: {},
130 | });
131 | return;
132 | }
133 | };
134 |
135 | return (
136 |
137 |
138 | {(succ) => (
139 |
140 |
141 |
Transaction success!
142 |
143 | You have sold {succ().numShareSold}{" "}
144 | {succ().type == "yes" ? "Yes" : "No"} shares for a total of of $
145 | {succ().usdOut.toFixed(2)}
146 |
147 |
148 | )}
149 |
150 |
151 |
152 | {(err) => (
153 |
154 |
155 |
Transaction failed!
156 |
157 | Your transaction could not be processed. Please try again later or
158 | contact support for assistance.
159 |
160 |
161 | )}
162 |
163 |
164 |
165 |
166 |
171 |
172 |
173 |
176 | Select an option to sell
177 |
178 | }
179 | when={option()}
180 | >
181 | {(o) => (
182 |
183 |
184 |
185 |
186 |
I want to sell
187 |
188 |
189 |
Yes shares
190 |
191 |
192 |
193 |
194 |
No shares
195 |
196 |
197 |
198 |
199 |
203 | Amount must be more than {MIN_SHARE_AMOUNT} shares
204 |
205 |
206 |
207 | Amount (shares)
208 |
209 |
{
214 | setAmount(
215 | Number.isNaN(e.currentTarget.valueAsNumber)
216 | ? undefined
217 | : e.currentTarget.valueAsNumber
218 | );
219 | }}
220 | type="number"
221 | class="bg-transparent outline-none text-right max-w-none flex-1 p-4 placeholder:text-neutral-500 min-w-0"
222 | placeholder="0"
223 | />
224 |
225 |
226 |
227 |
228 |
232 |
233 |
Sell for Total
234 |
${numDollarTotal()?.toFixed(0) ?? "N/A"}
235 |
236 |
237 |
Average Price
238 |
${avgPrice()?.toFixed(2) ?? "N/A"}
239 |
240 |
241 | {(p) => (
242 |
243 |
244 | Payout if{" "}
245 | {o().name} {" "}
246 | wins
247 |
248 |
249 |
250 | ${p().total.toFixed(2)}{" "}
251 |
252 | (+${p().earn.toFixed(2)})
253 |
254 |
255 |
256 | )}
257 |
258 | >
259 | }
260 | >
261 | {(e) => {e().message}
}
262 |
263 |
264 |
265 |
266 | {
268 | buy();
269 | }}
270 | data-type={type()}
271 | class="w-full
272 | data-[type=yes]:bg-blue-500
273 | data-[type=yes]:hover:bg-blue-700
274 | data-[type=no]:bg-red-500
275 | data-[type=no]:hover:bg-red-700
276 | transition-all
277 | text-white p-4 rounded font-bold"
278 | >
279 | Sell {type() === "yes" ? "Yes" : "No"}
280 |
281 |
282 |
283 | )}
284 |
285 |
286 |
287 | );
288 | }
289 |
--------------------------------------------------------------------------------
/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { TbLoader2 } from "solid-icons/tb";
2 |
3 | export default function Spinner(props: { size?: "sm" | "md" | "lg" }) {
4 | const sizeCls = {
5 | sm: "w-6 h-6",
6 | md: "w-8 h-8",
7 | lg: "w-10 h-10",
8 | }[props.size ?? "md"];
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/TranslatingGroup.tsx:
--------------------------------------------------------------------------------
1 | import { For, onMount } from "solid-js";
2 | import { infiniteScrollHovered, news } from "~/client/utils";
3 | import NewsItem from "./NewsItem";
4 |
5 | export default function TranslatingGroup(props: { "aria-hidden"?: boolean }) {
6 | let ref!: HTMLDivElement;
7 |
8 | onMount(() => {
9 | const width = ref.getBoundingClientRect().width;
10 | const SPEED = 10;
11 | const second = Math.floor(width / SPEED);
12 |
13 | ref.style.animation = `translating ${second}s linear infinite`;
14 | });
15 |
16 | return (
17 |
22 |
23 | {(n) => }
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/buysell/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Accessor, Show } from "solid-js";
2 | import OptionImage from "../OptionImage";
3 | import MarketImage from "../MarketImage";
4 |
5 | export default function Header(
6 | props: BuySellProps & {
7 | o: Accessor<{
8 | color: string;
9 | name: string;
10 | }>;
11 | }
12 | ) {
13 | return (
14 |
15 |
1}
17 | fallback={ }
18 | >
19 |
20 |
21 |
22 |
{props.market.name}
23 |
24 |
28 |
29 | {props.market.options.length == 1 ? "Yes" : props.o().name}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/entry-client.tsx:
--------------------------------------------------------------------------------
1 | // @refresh reload
2 | import { mount, StartClient } from "@solidjs/start/client";
3 |
4 | mount(() => , document.getElementById("app")!);
5 |
--------------------------------------------------------------------------------
/src/entry-server.tsx:
--------------------------------------------------------------------------------
1 | // @refresh reload
2 | import { createHandler, StartServer } from "@solidjs/start/server";
3 | import { getEnv } from "./server/utils";
4 |
5 | export default createHandler(() => {
6 | const clientKeys = ["INSTANTDB_APP_ID", "POSTHOG_TOKEN"];
7 | const env = clientKeys.reduce((acc, key) => {
8 | acc[key] = getEnv(key);
9 | return acc;
10 | }, {} as any);
11 |
12 | return (
13 | (
15 |
16 |
17 |
18 |
22 |
23 | Imply - Prediction Markets for AI & Humans
24 |
25 |
29 |
30 |
34 |
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
52 |
53 |
54 |
55 |
56 |
60 |
64 |
65 |
69 |
70 |
71 |
72 | {assets}
73 |
74 |
75 |
76 |
77 | {children}
78 | {scripts}
79 |
80 |
81 | )}
82 | />
83 | );
84 | });
85 |
--------------------------------------------------------------------------------
/src/routes/[...404].tsx:
--------------------------------------------------------------------------------
1 | import { A } from "@solidjs/router";
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
7 | Not Found
8 |
9 |
10 | Oops! Looks like the requested resource took a vacation. Try again
11 | later!
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/routes/api/alive/index.tsx:
--------------------------------------------------------------------------------
1 | export async function GET() {
2 | return new Response(`I'm alive!`, {
3 | status: 200,
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/src/routes/api/complete/index.ts:
--------------------------------------------------------------------------------
1 | import type { APIEvent } from "@solidjs/start/server";
2 | import { chat } from "~/server/chat-utils";
3 |
4 | import { streamNDJSON } from "~/shared/utils";
5 |
6 | export async function POST(event: APIEvent) {
7 | const body = (await event.request.json()) as APICompleteBody;
8 |
9 | const stream = streamNDJSON(chat(body));
10 |
11 |
12 | // Return the response with the transformed stream
13 | return new Response(stream, {
14 | headers: {
15 | "Content-Type": "application/json",
16 | "Transfer-Encoding": "chunked",
17 | },
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/routes/api/history__options/[id]/index.tsx:
--------------------------------------------------------------------------------
1 | import { id } from "@instantdb/admin";
2 | import type { APIEvent } from "@solidjs/start/server";
3 |
4 | import { createAdminDb } from "~/server/utils";
5 | import { yesProb } from "~/shared/utils";
6 |
7 | export async function POST(event: APIEvent) {
8 | const option_id = event.params.id;
9 | const db = createAdminDb();
10 |
11 | console.log("new history record for option", option_id);
12 |
13 | const resp = await db.query({
14 | options: {
15 | $: {
16 | where: {
17 | id: option_id,
18 | },
19 | },
20 | shares: {},
21 | },
22 | });
23 |
24 | const option = resp.options.at(0);
25 |
26 | if (!option) {
27 | return new Response(null, {
28 | status: 404,
29 | });
30 | }
31 |
32 | const yP = yesProb(option.shares);
33 |
34 | if (!yP) {
35 | return new Response(null, {
36 | status: 500,
37 | });
38 | }
39 |
40 | await db.transact([
41 | db.tx.history__options[id()].update({
42 | option_id: option_id,
43 | created_at: new Date().toISOString(),
44 | yesProb: yP,
45 | }),
46 | ]);
47 |
48 | console.log("created history record for option", option_id);
49 |
50 | return new Response(null, {
51 | status: 200,
52 | });
53 | }
54 |
--------------------------------------------------------------------------------
/src/routes/api/markets/[id]/vote/index.ts:
--------------------------------------------------------------------------------
1 | import { id } from "@instantdb/admin";
2 | import type { APIEvent } from "@solidjs/start/server";
3 | import { createAdminDb, getCookie, verifyJWT } from "~/server/utils";
4 |
5 | export async function POST(event: APIEvent) {
6 | const marketId = event.params.id;
7 | const body = await event.request.json();
8 | console.log("API: /api/markets/[id]/vote", marketId, body);
9 | const action = body as UpvoteDownvote;
10 | const profile_jwt = getCookie("profile_jwt");
11 | if (!profile_jwt)
12 | return new Response(null, { status: 401 /* Unauthorized */ });
13 | const payload = await verifyJWT(profile_jwt);
14 | if (!payload || !payload.profile_id || typeof payload.profile_id !== "string")
15 | return new Response(null, { status: 401 /* Unauthorized */ });
16 |
17 | if (!action || !action.type || !['upvote', 'downvote', 'remove'].includes(action.type))
18 | return new Response(null, { status: 400 /* BadRequest */ });
19 |
20 | const db = createAdminDb();
21 |
22 | const resp = await db.query({
23 | markets: {
24 | $: {
25 | where: {
26 | id: marketId,
27 | },
28 | },
29 | votes: {
30 | $: {
31 | where: {
32 | "profile.id": payload.profile_id,
33 | },
34 | },
35 | }
36 | },
37 | });
38 |
39 | const market = resp.markets?.at(0);
40 | if (!market)
41 | return new Response(null, {
42 | status: 404,
43 | });
44 | const vote = market.votes.at(0);
45 | if (action.type === 'remove') {
46 | // Already removed
47 | if (!vote)
48 | return new Response(null, { status: 200 });
49 |
50 | // Remove the vote
51 | await db.transact([
52 | db.tx.votes[vote.id].delete(),
53 | db.tx.markets[market.id].update({
54 | num_votes: market.num_votes + (vote.isUpvote ? -1 : 1),
55 | })
56 | ]);
57 | return new Response(null, { status: 200 });
58 | }
59 |
60 | const new_vote: Omit = {
61 | isUpvote: action.type === 'upvote',
62 | }
63 |
64 | // Already voted
65 | if (vote) {
66 | await db.transact([
67 | db.tx.votes[vote.id].update(new_vote),
68 | db.tx.markets[market.id].update({
69 | // Remove then add
70 | num_votes: market.num_votes + (vote.isUpvote ? -1 : 1) + (action.type === 'upvote' ? 1 : -1),
71 | })
72 | ]);
73 | return new Response(null, { status: 200 });
74 | }
75 |
76 |
77 | const vote_id = id()
78 | await db.transact([
79 | db.tx.votes[vote_id].update(new_vote).link({
80 | market: market.id,
81 | profile: payload.profile_id,
82 | }),
83 | db.tx.markets[market.id].update({
84 | // Just add
85 | num_votes: market.num_votes + (action.type === 'upvote' ? 1 : -1),
86 | })])
87 |
88 |
89 | return new Response(null, { status: 200 });
90 | }
--------------------------------------------------------------------------------
/src/routes/api/options/[id]/action/index.tsx:
--------------------------------------------------------------------------------
1 | import { id } from "@instantdb/admin";
2 | import type { APIEvent } from "@solidjs/start/server";
3 | import { createAdminDb, getCookie, verifyJWT } from "~/server/utils";
4 | import { buyShare, sellShare, triggerAddHistoryOption } from "~/shared/utils";
5 |
6 | export async function POST(event: APIEvent) {
7 | const optionId = event.params.id;
8 | const body = await event.request.json();
9 | const action = body as BuySellAction;
10 | const profile_jwt = getCookie("profile_jwt");
11 | if (!profile_jwt)
12 | return new Response(null, { status: 401 /* Unauthorized */ });
13 | const payload = await verifyJWT(profile_jwt);
14 | if (!payload || !payload.profile_id || typeof payload.profile_id !== "string")
15 | return new Response(null, { status: 401 /* Unauthorized */ });
16 |
17 | if (!action || !action.type || !action.amount || !action.shareId)
18 | return new Response(null, { status: 400 /* BadRequest */ });
19 |
20 | const db = createAdminDb();
21 |
22 | const resp = await db.query({
23 | options: {
24 | $: {
25 | where: {
26 | id: optionId,
27 | },
28 | },
29 | shares: {},
30 | },
31 | holdings: {
32 | $: {
33 | where: {
34 | "profile.id": payload.profile_id,
35 | "share.id": body.shareId,
36 | },
37 | },
38 | },
39 | profiles: {
40 | $: {
41 | where: {
42 | id: payload.profile_id,
43 | },
44 | },
45 | },
46 | });
47 |
48 | const profile = resp.profiles?.at(0);
49 | const option = resp.options?.at(0);
50 |
51 | if (!profile || !option)
52 | return new Response(null, {
53 | status: 404,
54 | });
55 |
56 | const share = option.shares.find((s) => s.id == body.shareId);
57 | const otherShare = option.shares.find((s) => s.id != body.shareId);
58 | if (!share || !otherShare) {
59 | return new Response(null, {
60 | status: 404,
61 | });
62 | }
63 |
64 | let holding: Holding;
65 | const respHolding = resp.holdings.at(0);
66 | if (!respHolding) {
67 | // Useful for testing
68 | console.log("creating zero holding...");
69 | const holding_id = id();
70 | let update = {
71 | amount: 0,
72 | updated_at: new Date().toISOString(),
73 | };
74 | await db.transact([
75 | db.tx.holdings[holding_id].update(update).link({
76 | profile: profile.id,
77 | share: body.shareId,
78 | }),
79 | ]);
80 |
81 | // Avoid another query
82 | holding = {
83 | id: holding_id,
84 | ...update,
85 | };
86 | } else {
87 | holding = respHolding;
88 | }
89 |
90 | let action_result: ShareActionResult | undefined;
91 | if (action.type == "buy") {
92 | if (action.amount > profile.usd) {
93 | return new Response(null, {
94 | status: 400,
95 | });
96 | }
97 |
98 | action_result = buyShare(option.shares, share.id, action.amount);
99 | if (!action_result)
100 | return new Response(null, {
101 | status: 400,
102 | });
103 | console.log("update balance, holding and share reserves...");
104 | await db.transact([
105 | db.tx.profiles[profile.id].update({
106 | // balance
107 | usd: profile.usd - action.amount,
108 | }),
109 | // holding
110 | db.tx.holdings[holding.id].update({
111 | amount: holding.amount + action_result.shareOut,
112 | updated_at: new Date().toISOString(),
113 | }),
114 | // share reserves
115 | db.tx.shares[share.id].update({
116 | reserve: action_result.shareReserve_after,
117 | }),
118 | db.tx.shares[otherShare.id].update({
119 | reserve: action_result.otherShareReserve_after,
120 | }),
121 | ]);
122 |
123 | console.log("updated, bought!", {
124 | profile_id: profile.id,
125 | holding_id: holding.id,
126 | share_id: share.id,
127 | other_share_id: otherShare.id,
128 | });
129 | }
130 |
131 | if (action.type == "sell") {
132 | if (action.amount > holding.amount) {
133 | return new Response(null, {
134 | status: 400,
135 | });
136 | }
137 |
138 | action_result = sellShare(option.shares, share.id, action.amount);
139 |
140 | if (!action_result)
141 | return new Response(null, {
142 | status: 400,
143 | });
144 |
145 | console.log("update balance, holding and share reserves");
146 | await db.transact([
147 | db.tx.profiles[profile.id].update({
148 | // balance
149 | usd: profile.usd + action_result.usdOut,
150 | }),
151 | // holding
152 | db.tx.holdings[holding.id].update({
153 | amount: holding.amount - action.amount,
154 | updated_at: new Date().toISOString(),
155 | }),
156 | // share reserves
157 | db.tx.shares[share.id].update({
158 | reserve: action_result.shareReserve_after,
159 | }),
160 | db.tx.shares[otherShare.id].update({
161 | reserve: action_result.otherShareReserve_after,
162 | }),
163 | ]);
164 |
165 | console.log("updated, sold!", {
166 | profile_id: profile.id,
167 | holding_id: holding.id,
168 | share_id: share.id,
169 | other_share_id: otherShare.id,
170 | });
171 | }
172 |
173 | if (!action_result) return new Response(null, { status: 400 });
174 |
175 | // trigger api history__options
176 | await triggerAddHistoryOption(option.id);
177 |
178 | return new Response(JSON.stringify(action_result), {
179 | headers: { "Content-Type": "application/json" },
180 | status: 200,
181 | });
182 | }
183 |
--------------------------------------------------------------------------------
/src/routes/api/profiles/jwt/index.tsx:
--------------------------------------------------------------------------------
1 | import { id } from "@instantdb/admin";
2 | import type { APIEvent } from "@solidjs/start/server";
3 | import { SignJWT } from "jose";
4 |
5 | import {
6 | createAdminDb,
7 | getCookie,
8 | setCookie,
9 | sign,
10 | verifyJWT,
11 | } from "~/server/utils";
12 |
13 | export async function GET(event: APIEvent) {
14 | const profile_jwt = getCookie("profile_jwt");
15 | const db = createAdminDb();
16 |
17 | if (profile_jwt) {
18 | try {
19 | const payload = await verifyJWT(profile_jwt);
20 |
21 | const resp = await db.query({
22 | profiles: {
23 | $: {
24 | where: {
25 | id: payload.profile_id as string,
26 | },
27 | },
28 | },
29 | });
30 |
31 | const profile = resp.profiles.at(0);
32 | if (profile) {
33 | return new Response(
34 | JSON.stringify({
35 | type: "existing",
36 | profile_id: payload.profile_id,
37 | } as JWTResult),
38 | {
39 | headers: { "Content-Type": "application/json" },
40 | status: 200,
41 | }
42 | );
43 | } else {
44 | console.warn("Profile not found", payload.profile_id);
45 | // Proceed to create a new profile
46 | }
47 | } catch (e) {
48 | console.log("JWT is invalid", e);
49 | setCookie("profile_jwt", "", { sameSite: "strict" });
50 | // Move forward to generate a new JWT
51 | }
52 | }
53 |
54 | const profile_id = id();
55 | const balance_id = id();
56 | console.log("creating profile...", profile_id, balance_id);
57 | try {
58 | await db.transact([
59 | db.tx.profiles[profile_id].update({
60 | avatar_src: "",
61 | name: "Guest",
62 | usd: 1000,
63 | }),
64 | ]);
65 | } catch (e) {
66 | console.error(JSON.stringify(e));
67 | return new Response(null, { status: 500 });
68 | }
69 |
70 | // I hereby allow whoever has this key to access holdings on this profile
71 | console.log("generating jwt...");
72 | const jwt = await sign(
73 | new SignJWT({
74 | sub: `guest-${profile_id}`,
75 | profile_id,
76 | })
77 | .setProtectedHeader({ alg: "HS256" })
78 | .setIssuedAt()
79 | );
80 |
81 | console.log("set cookie...");
82 | // console.log("set JWT:", jwt);
83 | // Set a new cookie (e.g., set a new profile key)
84 | setCookie("profile_jwt", jwt, {
85 | // maxAge: 60 * 60 * 24 * 7, // Expires in 7 days
86 | secure: true,
87 | httpOnly: true,
88 | sameSite: "strict",
89 | });
90 |
91 | console.log("DONE!");
92 | return new Response(
93 | JSON.stringify({
94 | type: "new",
95 | profile_id,
96 | } as JWTResult),
97 | {
98 | headers: { "Content-Type": "application/json" },
99 | status: 200,
100 | }
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { Show } from "solid-js";
2 | import { userChatted } from "~/client/utils";
3 | import AIComp from "../components/AIComp";
4 | import Markets from "../components/Markets";
5 |
6 | export default function Home() {
7 | return (
8 |
15 | {/*
16 |
17 |
18 |
19 |
*/}
20 |
21 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/routes/market/[id].tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "@solidjs/router";
2 | import { BsInfoCircle } from "solid-icons/bs";
3 | import { createEffect, For, onCleanup, onMount, Show } from "solid-js";
4 | import { db } from "~/client/database";
5 | import {
6 | market,
7 | optionId,
8 | profile,
9 | setHistoryOptionSubscription,
10 | setMarketSubscription,
11 | setOptionId,
12 | } from "~/client/utils";
13 | import BuySellComp from "~/components/BuySellComp";
14 | import MarketImage from "~/components/MarketImage";
15 | import MarketSocialComp from "~/components/MarketSocialComp";
16 | import MarketChart from "~/components/MartketChart";
17 | import OptionImage from "~/components/OptionImage";
18 | import { dateF_dmy, dateF_h, prob, probToPercent } from "~/shared/utils";
19 |
20 | export default function MarketPage() {
21 | const params = useParams();
22 |
23 | onMount(async () => {
24 | const unsub = db.subscribeQuery(
25 | {
26 | markets: {
27 | $: {
28 | where: {
29 | id: params.id,
30 | },
31 | },
32 | options: {
33 | shares: {},
34 | },
35 | },
36 | },
37 | (resp) => {
38 | console.log("market sub resp", resp);
39 | setMarketSubscription(resp);
40 | }
41 | );
42 |
43 | onCleanup(() => {
44 | unsub();
45 | });
46 | });
47 |
48 | createEffect(() => {
49 | const optionIds = market()?.options.map((o) => o.id);
50 | if (!optionIds || optionIds.length == 0) return;
51 |
52 | const unsub = db.subscribeQuery(
53 | {
54 | history__options: {
55 | $: {
56 | limit: 1000,
57 | order: {
58 | serverCreatedAt: "desc",
59 | },
60 | where: {
61 | option_id: {
62 | $in: optionIds,
63 | },
64 | },
65 | },
66 | },
67 | },
68 | (resp) => {
69 | console.log("share history sub resp", resp);
70 | setHistoryOptionSubscription(resp);
71 | }
72 | );
73 |
74 | onCleanup(() => {
75 | unsub();
76 | });
77 | });
78 |
79 | return (
80 |
81 | {(m) => (
82 |
83 |
84 |
85 |
86 |
87 |
{m().name}
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | AI will resolve this market at: {dateF_dmy(m().resolve_at)}
99 |
100 |
101 | Stop trading at: {dateF_dmy(m().stop_trading_at)} (
102 | {dateF_h(m().stop_trading_at)})
103 |
104 |
105 |
106 |
107 |
108 |
109 |
Description
110 |
{m().description}
111 |
112 |
113 |
Rule
114 |
{m().rule}
115 |
116 |
117 |
118 |
1}>
119 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | )}
145 |
146 | );
147 | }
148 |
--------------------------------------------------------------------------------
/src/routes/profile/[id].tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "@solidjs/router";
2 | import { createSignal, For, onMount, Show } from "solid-js";
3 | import ProfileImage from "~/components/ProfileImage";
4 | import { db } from "~/client/database";
5 | import { dateF } from "~/shared/utils";
6 |
7 | export default function ProfilePage() {
8 | const [profileResponse, setProfileResponse] = createSignal();
9 | const profile = () => profileResponse()?.data.profiles.at(0);
10 | const params = useParams();
11 |
12 | onMount(async () => {
13 | const resp = await db.queryOnce({
14 | profiles: {
15 | $: {
16 | where: {
17 | id: params.id,
18 | },
19 | },
20 | holdings: {
21 | share: {
22 | option: {
23 | market: {},
24 | },
25 | },
26 | },
27 | },
28 | });
29 | // TODO: For some reason share can be undefined, fix
30 | setProfileResponse(resp as any);
31 | });
32 | return (
33 |
34 | {(p) => (
35 |
36 |
37 |
Profile
38 |
39 |
40 |
41 |
{p().name}
42 |
${p().usd.toFixed(2)}
43 |
44 |
45 |
46 |
47 |
Holdings
48 |
49 |
50 |
51 |
52 |
53 | Amount
54 | Yes/No
55 | Option
56 | Market
57 | Updated At
58 |
59 |
60 |
61 |
62 | {(h) => (
63 |
64 | {h.amount.toFixed(2)}
65 | {h.share.type == "no" ? "No" : "Yes"}
66 | {h.share.option.name}
67 |
68 |
72 | {h.share.option.market.name}
73 |
74 |
75 | {dateF(h.updated_at)}
76 |
77 | )}
78 |
79 |
80 |
81 |
82 |
83 |
84 | )}
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/src/server/chat-utils.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from "openai";
2 | import { ChatCompletionChunk, ChatCompletionMessageToolCall, ChatModel } from "openai/resources/index.mjs";
3 | import { ChatCompletionMessageParam } from "openai/src/resources/index.js";
4 | import { parallelMerge } from 'streaming-iterables';
5 |
6 | import { Stream } from "openai/streaming.mjs";
7 |
8 | import { tools } from "~/shared/tools";
9 | import { ToolName } from "~/shared/tools/utils";
10 | import { getRandomKaomoji } from "~/shared/utils";
11 | import { getEnv } from "./utils";
12 | import { getRequestEvent } from "solid-js/web";
13 |
14 | function createClient() {
15 | const apiKey = getEnv("OPENAI_API_KEY");
16 | const baseURL = getEnv("OPENAI_BASE_URL");
17 | const client = new OpenAI({
18 | apiKey,
19 | baseURL,
20 | defaultHeaders: {
21 | "HTTP-Referer": "https://imply.app",
22 | "X-Title": "Imply",
23 | }
24 | });
25 | return client
26 | }
27 |
28 | function check_if_tools_done(
29 | tool_records: { [id: string]: ToolRecord },
30 | prev: string[],
31 | now: string[]
32 | ): HighLevelMessage[] {
33 | const done_ids = prev.filter(id => !now.includes(id))
34 |
35 | return done_ids.map(id => {
36 | const record = tool_records[id];
37 | return {
38 | doing: {
39 | tool: {
40 | done: {
41 | id,
42 | name: record.name,
43 | arguments: JSON.parse(record.arguments_str),
44 | created_at: record.created_at,
45 | updated_at: new Date().toISOString()
46 | }
47 | }
48 | }
49 | }
50 | })
51 | }
52 |
53 | function check_if_content_done(content_record: ContentRecord): HighLevelMessage | undefined {
54 | if (content_record.created_at) return {
55 | doing: {
56 | content: {
57 | done: {
58 | content: content_record.content,
59 | created_at: content_record.created_at,
60 | id: content_record.id,
61 | updated_at: new Date().toISOString()
62 | }
63 | }
64 | }
65 | }
66 | }
67 |
68 |
69 | function check_if_reasoning_done(reasoning_record: ReasoningRecord): HighLevelMessage | undefined {
70 | if (reasoning_record.created_at) return {
71 | doing: {
72 | reasoning: {
73 | done: {
74 | content: reasoning_record.content,
75 | created_at: reasoning_record.created_at,
76 | id: reasoning_record.id,
77 | updated_at: new Date().toISOString()
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
84 |
85 | async function getCompletionWithTools(messages: ChatCompletionMessageParam[], content_only: boolean) {
86 | const event = getRequestEvent();
87 |
88 | // Use OpenAI to structure the tool call output
89 | const client = createClient()
90 | const model: ChatModel = getEnv("OPENAI_MODEL") ?? 'openai/gpt-4o-mini'
91 | const completion = await client.chat.completions.
92 | // @ts-ignore
93 | create({
94 | model,
95 | messages: [
96 | content_only ? systemMessage().content : systemMessage().tools,
97 | ...messages
98 | ],
99 | tools: content_only ? undefined : tools.map((t) => t.definition),
100 | stream: true,
101 |
102 | });
103 |
104 | event?.request.signal.addEventListener('abort', () => {
105 | console.log('getCompletionWithTools abort')
106 | completion.controller.abort()
107 | })
108 |
109 | return completion
110 | }
111 |
112 | async function getReasoningCompletion(messages: ChatCompletionMessageParam[]) {
113 | const event = getRequestEvent();
114 |
115 | // Use DeepSeek to reasoning and chat with user
116 | const client = createClient()
117 | const model: ChatModel = getEnv("REASONING_MODEL") ?? 'deepseek/deepseek-r1:free'
118 | const completion = await client.chat.completions.
119 |
120 | // @ts-ignore
121 | create({
122 | model,
123 | messages: [
124 | systemMessage().reasoning,
125 | ...messages
126 | ],
127 | // tools: tools.map((t) => t.definition),
128 | stream: true,
129 | include_reasoning: true
130 | });
131 |
132 |
133 | event?.request.signal.addEventListener('abort', () => {
134 | console.log('getReasoningCompletion abort')
135 | completion.controller.abort()
136 | })
137 | return completion
138 | }
139 |
140 |
141 | // Take in a stream of chunks
142 | // combine them into higher level updates
143 | export async function* parseOpenAIChunk(
144 | chunks: Stream
145 | ): AsyncGenerator {
146 | const this_id = new Date().toISOString();
147 | const reasoning_record: ReasoningRecord = {
148 | id: `${this_id}/reasoning`,
149 | content: ''
150 | }
151 |
152 | const content_record: ContentRecord = {
153 | id: `${this_id}/content`,
154 | content: ''
155 | }
156 |
157 | const tool_records:
158 | { [id: string]: ToolRecord }
159 | = {
160 | }
161 |
162 | // Gemini sends empty tool_call.id, so we cannot use tool_call.id to identity the call. Use a timestamp + index instead.
163 | const to_id = (tool_call: OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta.ToolCall) => `${this_id}/${tool_call.index}`
164 |
165 | // FAST SIGNALING
166 | // Detects when a tool call is done and yield immediately
167 | let prev_tool_ids: string[] = []
168 | let tool_called = false;
169 |
170 | for await (const chunk of chunks) {
171 | // console.log("chunk", JSON.stringify(chunk));
172 |
173 | const delta: ChatCompletionChunk.Choice.Delta & { reasoning?: string } = chunk.choices[0].delta;
174 |
175 |
176 | if (delta.tool_calls) {
177 | // FAST CHECK
178 | const r = check_if_reasoning_done(reasoning_record); if (r) yield r;
179 | const m = check_if_content_done(content_record); if (m) yield m;
180 | const now_tool_ids = delta.tool_calls.map(to_id)
181 | yield* check_if_tools_done(tool_records, prev_tool_ids, now_tool_ids)
182 | prev_tool_ids = now_tool_ids
183 |
184 |
185 | for (const tool_call of delta.tool_calls) {
186 | tool_called = true
187 | if (!tool_call.function) throw new Error('No function name what')
188 |
189 | // Update inner map
190 | const id = to_id(tool_call)
191 | let prev = tool_records[id];
192 | let now: ToolRecord;
193 | if (!prev) {
194 | now = {
195 | name: tool_call.function.name! as ToolName,
196 | arguments_str: tool_call.function.arguments ?? '',
197 | created_at: new Date().toISOString()
198 | }
199 |
200 | yield {
201 | doing: {
202 | tool: {
203 | started: {
204 | id,
205 | created_at: now.created_at,
206 | name: now.name
207 | }
208 | }
209 | }
210 | }
211 | } else {
212 | now = {
213 | ...prev,
214 | arguments_str: prev.arguments_str + tool_call.function.arguments!,
215 | created_at: new Date().toISOString()
216 | }
217 |
218 | yield {
219 | doing: {
220 | tool: {
221 | delta: {
222 | id,
223 | created_at: now.created_at,
224 | name: now.name,
225 | arguments_delta: tool_call.function.arguments ?? '',
226 | updated_at: new Date().toISOString()
227 | }
228 | }
229 | }
230 | }
231 | }
232 |
233 |
234 | tool_records[id] = now
235 | }
236 | }
237 |
238 | if (delta.content) {
239 | // FAST CHECK
240 | const r = check_if_reasoning_done(reasoning_record); if (r) yield r;
241 | yield* check_if_tools_done(tool_records, prev_tool_ids, [])
242 |
243 |
244 | if (content_record.created_at) {
245 | yield {
246 | doing: {
247 | content: {
248 | delta: {
249 | created_at: content_record.created_at,
250 | id: content_record.id,
251 | text: delta.content,
252 | updated_at: new Date().toISOString()
253 | }
254 | }
255 | }
256 | }
257 | } else {
258 | const created_at = new Date().toISOString()
259 | content_record.content = delta.content;
260 | content_record.created_at = created_at
261 | yield {
262 | doing: {
263 | content: {
264 | started: {
265 | id: content_record.id,
266 | created_at,
267 | text: delta.content
268 | }
269 | }
270 | }
271 | }
272 | }
273 |
274 | content_record.content += delta.content;
275 | }
276 |
277 | if (delta.reasoning) {
278 | // FAST CHECK
279 | const m = check_if_content_done(content_record); if (m) yield m;
280 | yield* check_if_tools_done(tool_records, prev_tool_ids, [])
281 |
282 | if (reasoning_record.created_at) {
283 | yield {
284 | doing: {
285 | reasoning: {
286 | delta: {
287 | created_at: reasoning_record.created_at,
288 | id: reasoning_record.id,
289 | text: delta.reasoning,
290 | updated_at: new Date().toISOString()
291 | }
292 | }
293 | }
294 | }
295 | } else {
296 | const created_at = new Date().toISOString()
297 | reasoning_record.content = delta.reasoning;
298 | reasoning_record.created_at = created_at
299 | yield {
300 | doing: {
301 | reasoning: {
302 | started: {
303 | id: reasoning_record.id,
304 | created_at,
305 | text: delta.reasoning
306 | }
307 | }
308 | }
309 | }
310 | }
311 |
312 | reasoning_record.content += delta.reasoning;
313 | }
314 | }
315 |
316 |
317 | const r = check_if_reasoning_done(reasoning_record); if (r) yield r;
318 | const m = check_if_content_done(content_record); if (m) yield m;
319 | yield* check_if_tools_done(tool_records, prev_tool_ids, [])
320 | yield {
321 | done: {
322 | tool_called
323 | }
324 | }
325 | }
326 |
327 | export const prepare = (messages: ChatCompletionMessageParam[]) => {
328 | const isToolCall = (m: ChatCompletionMessageParam) => m.role == 'assistant' && m.tool_calls && m.tool_calls.length > 0
329 | const isToolResult = (m: ChatCompletionMessageParam) => m.role == 'tool'
330 | let maybeToolCall_i = messages.length - 1;
331 | while (maybeToolCall_i >= 0 && isToolResult(messages[maybeToolCall_i])) { maybeToolCall_i-- }
332 | const foundToolCall = maybeToolCall_i >= 0 && isToolCall(messages[maybeToolCall_i]);
333 | const toolCallToNormalMessage = (m: ChatCompletionMessageParam): ChatCompletionMessageParam => {
334 | if (!(m.role == 'assistant' && m.tool_calls && m.tool_calls.length > 0)) throw new Error('Impossible!')
335 | const tool_call = m.tool_calls![0];
336 | return {
337 | role: 'assistant',
338 | content: `I called the tool: ${tool_call.function.name} with arguments: ${tool_call.function.arguments}`
339 | }
340 | }
341 |
342 | const toolResultToNormalMessage = (m: ChatCompletionMessageParam): ChatCompletionMessageParam => {
343 | if (!(m.role == 'tool')) throw new Error('Impossible!')
344 | return {
345 | role: 'assistant',
346 | content: `The tool returned: ${m.content}`
347 | }
348 | }
349 |
350 | let budget = 14000;
351 | const prepared_belows: ChatCompletionMessageParam[] = []
352 | if (foundToolCall) {
353 | let belowBudget = 10000;
354 |
355 | for (let i = maybeToolCall_i + 1; i < messages.length; i++) {
356 | let prepared_tool_result = { ...messages[i] };
357 | if (!isToolResult(prepared_tool_result)) throw new Error('Impossible!')
358 | // Hack: try divide the budget among the tool results
359 | const max_each = Math.ceil(belowBudget / (messages.length - i));
360 | const saved = Math.max(prepared_tool_result.content.length - max_each, 0)
361 | prepared_tool_result.content = prepared_tool_result.content.slice(-max_each);
362 | belowBudget += saved - max_each;
363 |
364 | prepared_tool_result = toolResultToNormalMessage(prepared_tool_result)
365 |
366 | prepared_belows.push(prepared_tool_result)
367 | }
368 |
369 | budget -= belowBudget;
370 | }
371 |
372 | const prepared_aboves: ChatCompletionMessageParam[] = []
373 | let keep_assistant_content_short = 200;
374 | const above_end_i = foundToolCall ? maybeToolCall_i : messages.length;
375 | for (let i = 0; i < above_end_i; i++) {
376 | const prepared_m = { ...messages[i] }
377 | if (prepared_m.role === 'assistant') {
378 | // Hack: Do not keep track of assistant old calls
379 | if (prepared_m.tool_calls) continue
380 | if (prepared_m.content) {
381 | prepared_m.content = prepared_m.content.slice(-Math.max(budget, keep_assistant_content_short));
382 | budget -= prepared_m.content.length;
383 | }
384 |
385 | prepared_aboves.push(prepared_m)
386 | if (budget == 0) break;
387 | }
388 |
389 | if (prepared_m.role === 'user') {
390 | prepared_m.content = prepared_m.content.slice(-budget);
391 |
392 | prepared_aboves.push(prepared_m)
393 | if (budget == 0) break;
394 | }
395 | }
396 |
397 | const preparedToolCalls = foundToolCall ? [
398 | toolCallToNormalMessage(messages[maybeToolCall_i])
399 | ] : []
400 | return [
401 | ...prepared_aboves,
402 | // Raw tool call message
403 | ...preparedToolCalls,
404 | ...prepared_belows,
405 | ]
406 | };
407 |
408 | export const systemMessage = (): { [key: string]: ChatCompletionMessageParam } => ({
409 | reasoning: {
410 | role: "system",
411 | content: `You are the native PLANNING AI of Imply.app
412 | Your job is to work together with the TOOLING AI to estimate probabilities & help create prediction markets. Your response will be forward to the TOOLING AI instead directly to the user. Give details step by step instruction. TOOLING AI's response will then be forward back to you.
413 |
414 | IMPORTANT: ALWAYS call the TOOLING AI to search for news first!
415 |
416 | Imply.app is a prediction market platform for everyone (no topic is off-limits!).
417 | The app uses play money (still called USD).
418 |
419 | Current time: ${new Date().toISOString()}
420 |
421 | Do not be creative! Research properly! Be critical! Be detailed!
422 | Challenge the possibility of the event!`,
423 | },
424 | tools: {
425 | role: "system",
426 | content: `You are the native TOOLING AI of Imply.app—a prediction market platform for everyone. The app uses play money(still called USD).
427 |
428 | Research properly using search news tools! Be critical! Be detailed! Challenge the possibility of the event!
429 |
430 | Your job is to work together with PLANNING AI to:
431 | 1. Estimate probabilities: Guess how accurate predictions are.
432 | 2. Help create prediction markets.
433 |
434 | Current time: ${new Date().toISOString()}.
435 | `,
436 | },
437 | content: {
438 | role: "system",
439 | content: `You are the native AI of Imply.app—a prediction market platform for everyone(no topic is off - limits!).The app uses play money(still called USD).
440 |
441 | Your job is to work together with PLANNING AI to:
442 | 1. Estimate probabilities: Guess how accurate predictions are(e.g., "That's quite improbable! I give it 22% probability.").
443 | 2. Help create prediction markets.
444 |
445 | Current time: ${new Date().toISOString()}
446 | Use bold text to emphasize important points.
447 | DO NOT OUTPUT LINKS IN YOUR RESPONSE.`,
448 | },
449 | });
450 |
451 | export async function* chat(body: APICompleteBody): AsyncGenerator {
452 | const history: ChatCompletionMessageParam[] = body.blocks.map((b) => {
453 | return {
454 | role: b.role,
455 | content: b.content,
456 | } as any;
457 | });
458 |
459 | if (history.length === 1) {
460 | history[0] = {
461 | ...history[0],
462 | content: `My prediction is: ${history[0].content} `,
463 | }
464 | }
465 |
466 | let messages = history;
467 | // Only tools in the same "inference" can share memory
468 | // To share memory accross inferences we need to store memStorage to something persistent
469 | // like an in-memory database like Redis or a real database (Postgres, InstantDB)
470 | const extraArgs: ExtraArgs = {
471 | memStorage: {}
472 | }
473 |
474 | // Safe guard
475 | let MAX_ITER = 5;
476 | let tool_called: ToolName[] = []
477 | try {
478 | while (MAX_ITER--) {
479 | messages = prepare(messages);
480 | // console.log('prepared messages', JSON.stringify(messages));
481 | const tool_calls: NonNullable['done']>[] = []
482 | let tool_args = false;
483 | let reasoning_text = ''
484 | const just_need_announce = tool_called.includes(ToolName.createMarket)
485 |
486 | // STEP 0: Planning with Reasoning Model
487 | // Do nothing reasoning if just created market, just announce to user
488 | if (just_need_announce) {
489 | console.log('createMarket tool called');
490 | } else {
491 | console.log('reasoning...');
492 | for await (const update of parseOpenAIChunk(
493 | await getReasoningCompletion(messages)
494 | )) {
495 | if (update.doing) {
496 |
497 | yield {
498 | ...update.doing,
499 | agent_step: 'reasoning_and_foward'
500 | }
501 |
502 | if (update.doing.reasoning?.done) {
503 | reasoning_text = update.doing.reasoning.done.content
504 | }
505 |
506 | if (update.doing.content?.done) {
507 | const msg: ChatCompletionMessageParam = {
508 | role: "assistant",
509 | content: `
510 | ${reasoning_text}
511 |
512 | ${update.doing.content.done.content} `
513 | }
514 | messages.push(msg)
515 | }
516 | }
517 | }
518 | }
519 |
520 | tool_called = []
521 |
522 | // STEP 1: Build arguments for tool calls or generate content
523 | const chunks = await getCompletionWithTools(messages, just_need_announce)
524 |
525 | for await (const update of parseOpenAIChunk(chunks)) {
526 | if (update.doing) {
527 | yield {
528 | ...update.doing,
529 | agent_step: 'tool_call_and_content'
530 | }
531 |
532 | if (update.doing.tool?.done) {
533 | console.log('update tool done', update.doing.tool.done);
534 | tool_calls.push(update.doing.tool.done)
535 | tool_args = true
536 | }
537 |
538 | if (update.doing.content?.done) {
539 | const msg: ChatCompletionMessageParam = {
540 | role: "assistant",
541 | content: update.doing.content.done.content
542 | }
543 | messages.push(msg)
544 | }
545 | }
546 |
547 | if (update.done) {
548 | tool_args = update.done.tool_called
549 | }
550 | }
551 |
552 | const tool_calls_fixed = tool_calls.map(t => {
553 | const c: ChatCompletionMessageToolCall = {
554 | id: t.id,
555 | type: 'function',
556 | function: {
557 | arguments: JSON.stringify(t.arguments),
558 | name: t.name
559 | },
560 | }
561 | return c;
562 | })
563 |
564 | const m: ChatCompletionMessageParam = {
565 | role: 'assistant',
566 | tool_calls: tool_calls_fixed
567 | }
568 |
569 | messages.push(m)
570 | // STEP 2: Actually execute the tool logic
571 | if (tool_args) {
572 | const generators = tool_calls.map((async function* f(tool_call) {
573 | const toolLogic = tools.find(t => t.definition.function.name === tool_call.name)?.function;
574 | if (!toolLogic) {
575 | throw new Error(`Tool ${tool_call.name} not found`);
576 | }
577 | tool_called.push(tool_call.name)
578 |
579 | const toolG = toolLogic(tool_call.arguments, extraArgs);
580 |
581 | for await (const tool_yield of toolG) {
582 | yield { id: tool_call.id, ...tool_yield, name: tool_call.name } as ToolYieldWithId
583 | }
584 | }))
585 |
586 | const g = parallelMerge(...generators)
587 | console.log('waiting for tool calls...');
588 | for await (const tool_yield of g) {
589 | yield {
590 | tool_yield,
591 | agent_step: 'tool_call_and_content'
592 | }
593 |
594 | // AI only see done messages
595 | if (tool_yield.done) {
596 | messages.push({
597 | role: 'tool',
598 | content: JSON.stringify(tool_yield.done),
599 | tool_call_id: tool_yield.id
600 | })
601 | }
602 | }
603 |
604 | continue
605 | }
606 |
607 | break;
608 | }
609 |
610 | console.log(">> ALL DONE");
611 | } catch (e) {
612 | console.error(e);
613 | }
614 | }
615 |
--------------------------------------------------------------------------------
/src/server/utils.ts:
--------------------------------------------------------------------------------
1 | import { jwtVerify as jose_jwtVerify, SignJWT } from "jose";
2 | import { getRequestEvent } from "solid-js/web";
3 |
4 |
5 | import { init } from "@instantdb/admin";
6 | import {
7 | getCookie as vinxi_getCookie,
8 | setCookie as vinxi_setCookie,
9 | } from "vinxi/http";
10 | import schema from "~/../instant.schema";
11 |
12 | export function getEnv(key: string) {
13 | const event = getRequestEvent();
14 | return event?.nativeEvent.context.cloudflare?.env[key] ?? process.env[key];
15 | }
16 |
17 | export function createAdminDb() {
18 | const INSTANT_APP_ADMIN_TOKEN = getEnv("INSTANT_APP_ADMIN_TOKEN");
19 | const INSTANTDB_APP_ID = getEnv("INSTANTDB_APP_ID");
20 | const db = init({
21 | appId: INSTANTDB_APP_ID,
22 | adminToken: INSTANT_APP_ADMIN_TOKEN,
23 | schema,
24 | });
25 | return db;
26 | }
27 | export function getCookie(key: string) {
28 | const event = getRequestEvent();
29 | if (!event) throw new Error("No event!");
30 | const value = vinxi_getCookie(event.nativeEvent, key);
31 | return value;
32 | }
33 |
34 | export function setCookie(key: string, value: string, options?: any) {
35 | const event = getRequestEvent();
36 | if (!event) throw new Error("No event!");
37 | vinxi_setCookie(event.nativeEvent, key, value, {
38 | secure: true,
39 | httpOnly: true,
40 | sameSite: "strict",
41 | ...options,
42 | });
43 | }
44 |
45 | export async function verifyJWT(profile_jwt: string) {
46 | const JWT_SECRET_KEY = getEnv("JWT_SECRET_KEY");
47 | const secret = new TextEncoder().encode(JWT_SECRET_KEY);
48 |
49 | const { payload } = await jose_jwtVerify(profile_jwt, secret);
50 | return payload;
51 | }
52 |
53 | export async function sign(jwtObject: SignJWT) {
54 | const JWT_SECRET_KEY = getEnv("JWT_SECRET_KEY");
55 | const secret = new TextEncoder().encode(JWT_SECRET_KEY);
56 | return jwtObject.sign(secret);
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/shared/tools/createMarket.ts:
--------------------------------------------------------------------------------
1 | import { id } from "@instantdb/admin";
2 | import { z } from "zod";
3 | import { createAdminDb } from "~/server/utils";;
4 | import { createOption, notEmpty, retry_if_fail, triggerAddHistoryOption } from "../utils";
5 | import { makeTool, ToolName } from "./utils";
6 | import { fetchImages } from "./searchImages";
7 |
8 | const schema = z.object({
9 | name: z.string(),
10 | description: z.string(),
11 | rule: z.string(),
12 | type: z.enum(["binary", "multiple"]),
13 | thumbnail_query: z.string(),
14 | resolve_at: z.date(),
15 | // Binary market fields
16 | probability_yes: z.number().optional(),
17 |
18 | // Multiple market fields
19 | allow_multiple_correct: z.boolean().optional(),
20 | options: z
21 | .array(
22 | z.object({
23 | name: z.string(),
24 | probability_yes: z.number(),
25 | })
26 | )
27 | .optional(),
28 | });
29 |
30 | export type CreateMarketToolArgs = z.infer;
31 | export type CreateMarketToolDone = ExtractType<'done', typeof createMarket>;
32 |
33 | async function* createMarket({
34 | name,
35 | type,
36 | probability_yes,
37 | options,
38 | allow_multiple_correct,
39 | description,
40 | rule,
41 | resolve_at,
42 | thumbnail_query
43 | }: CreateMarketToolArgs) {
44 | const db = createAdminDb();
45 | const market_id = id();
46 |
47 | let marketOptions: ReturnType[] | undefined = undefined;
48 |
49 | if (type === "multiple") {
50 | if (!options || options.length === 0) {
51 | throw new Error("Multiple-option markets must include at least one 'option'.");
52 | }
53 |
54 | marketOptions = options.map((o) => createOption(db, o.name, o.probability_yes));
55 | }
56 |
57 | if (type == "binary") {
58 | if (probability_yes === undefined) {
59 | throw new Error("Binary market must include 'probability_yes'");
60 | }
61 | marketOptions = [createOption(db, "o_only", probability_yes)];
62 | }
63 |
64 | if (!marketOptions || marketOptions.length === 0) {
65 | throw new Error("No marketOptions");
66 | }
67 |
68 | let n = 5;
69 | let response: Response | undefined = undefined;
70 | while (n--) {
71 | try {
72 | response = await fetchImages(thumbnail_query);
73 | if (response.ok) {
74 | break;
75 | }
76 |
77 | } catch (e) {
78 | await new Promise((resolve) => setTimeout(resolve, 1000 + Math.floor(Math.random() * 1000)));
79 | }
80 | }
81 |
82 | if (!response || !response.ok) {
83 | throw new Error("Failed to fetch news");
84 | }
85 |
86 | let image_response: Response | undefined = undefined;
87 |
88 | image_response = await retry_if_fail(() => { return fetchImages(thumbnail_query) });
89 | if (!image_response || !image_response.ok) {
90 | throw new Error("Failed to fetch images");
91 | }
92 |
93 | const data = await image_response.json() as BraveSearchImagesRoot;
94 | const img_result = data.results[Math.floor(Math.random() * 3)];
95 |
96 | console.log('img_result', img_result)
97 | const res_at = new Date(resolve_at).toISOString();
98 | const stop_trading_at = new Date(new Date(resolve_at).getTime() - 2 * 24 * 60 * 60 * 1000).toISOString();
99 |
100 | const transactions: Parameters[0] = [
101 | ...marketOptions.flatMap((o) => o.transactions),
102 | db.tx.markets[market_id]
103 | .update({
104 | name,
105 | description,
106 | image: img_result.thumbnail.src,
107 | allow_multiple_correct,
108 | created_at: new Date().toISOString(),
109 | resolve_at: res_at,
110 | stop_trading_at,
111 | rule,
112 | num_votes: 0,
113 | })
114 | .link({
115 | options: marketOptions.map((o) => o.option_id),
116 | }),
117 | ];
118 |
119 | try {
120 | await db.transact(transactions);
121 | const ps = marketOptions.map((o) => triggerAddHistoryOption(o.option_id));
122 | await Promise.all(ps);
123 | } catch (e) {
124 | console.error("error", e);
125 | }
126 |
127 | yield {
128 | done: { market_id },
129 | }
130 | }
131 |
132 |
133 | export const createMarketTool = makeTool({
134 | name: ToolName.createMarket,
135 | zodObj: schema,
136 | function: createMarket,
137 | description: `Create a prediction market.
138 | Important:
139 | - Probability must be in range [0, 1].
140 | - Description are long (200 words) paragraphs of text to give a context to the market. It should explain why initial probability is set to a certain value. Be insightful and concise.
141 |
142 | The 'type' field determines the market structure:
143 | - 'binary' requires 'probability_yes'. Important: try to estimate it accurately.
144 | - 'multiple' requires 'options' with at least one entry.
145 |
146 | The 'allow_multiple_correct' field determines how options are treated:
147 | - **false**: Only one option can be true (e.g., "Who will win the tournament?").
148 | - **true**: Multiple options can be correct (e.g., "Will Bitcoin hit $200k?" for different months).
149 |
150 | - name: Extremely specific question (e.g., "Will Bitcoin hit $200k by 2023?").
151 | - rules: Clear and unambiguous. Mention specific data source for market resolution.`,
152 | });
153 |
--------------------------------------------------------------------------------
/src/shared/tools/index.ts:
--------------------------------------------------------------------------------
1 | import { createMarketTool } from "./createMarket";
2 | import { searchImagesTool } from "./searchImages";
3 | import { searchWebTool } from "./searchWeb";
4 |
5 |
6 | export const tools = [
7 | // searchWeatherTool,
8 | createMarketTool,
9 | searchWebTool,
10 | searchImagesTool,
11 |
12 | ]
--------------------------------------------------------------------------------
/src/shared/tools/searchImages.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { getEnv } from "~/server/utils";
3 | import { id } from "@instantdb/admin";
4 | import { makeTool, ToolName } from "./utils";
5 | import { retry_if_fail } from "../utils";
6 |
7 | const schema = z.object({
8 | query: z.string(),
9 | });
10 |
11 | export type SearchImagesToolArgs = z.infer;
12 | export type SearchImagesToolDone = ExtractType<'done', typeof searchImages>;
13 | export type SearchImagesToolDoing = ExtractType<'doing', typeof searchImages>;
14 |
15 | export async function fetchImages(query: string) {
16 | const response = await fetch(
17 | `https://api.search.brave.com/res/v1/images/search?q=${encodeURIComponent(
18 | query
19 | )}&count=6&search_lang=en&safesearch=strict&spellcheck=1`,
20 | {
21 | headers: {
22 | "Accept": "application/json",
23 | "Accept-Encoding": "gzip",
24 | "X-Subscription-Token": getEnv("BRAVE_SEARCH_API_KEY"),
25 | },
26 | }
27 | );
28 |
29 | if (!response.ok) {
30 | console.warn("response error, retry");
31 | throw new Error("Failed to fetch images");
32 | }
33 |
34 | return response;
35 | }
36 |
37 | async function* searchImages({ query }: SearchImagesToolArgs) {
38 |
39 | let response: Response | undefined = undefined;
40 |
41 | response = await retry_if_fail(() => { return fetchImages(query) });
42 | if (!response || !response.ok) {
43 | throw new Error("Failed to fetch images");
44 | }
45 |
46 | const data = await response.json() as BraveSearchImagesRoot;
47 | const confidenceScore = {
48 | high: 1,
49 | medium: 0.5,
50 | low: 0,
51 | };
52 |
53 | const dataBindId = {
54 | ...data,
55 | results: data.results
56 | .toSorted((a, b) => confidenceScore[a.confidence] - confidenceScore[b.confidence])
57 | .map((r) => ({
58 | ...r,
59 | image_uuid: id(),
60 | }))
61 | }
62 |
63 | // only extract a small surface to give the AI (save token)
64 | const images = dataBindId.results
65 | .map((r) => ({
66 | image_uuid: r.image_uuid,
67 | title: r.title,
68 | host: new URL(r.url).host,
69 | }));
70 |
71 | yield {
72 | doing: {
73 | data: dataBindId
74 | }
75 | }
76 |
77 | yield {
78 | done: images,
79 | }
80 | }
81 |
82 | export const searchImagesTool = makeTool({
83 | name: ToolName.searchImages,
84 | zodObj: schema,
85 | function: searchImages,
86 | });
87 |
--------------------------------------------------------------------------------
/src/shared/tools/searchWeb.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { getEnv } from "~/server/utils";
3 | import { makeTool, ToolName } from "./utils";
4 | import { retry_if_fail } from "../utils";
5 |
6 | const schema = z.object({
7 | query: z.string(),
8 | });
9 |
10 | export type SearchWebToolArgs = z.infer;
11 | export type SearchWebToolDone = ExtractType<'done', typeof searchWeb>;
12 |
13 | async function fetchNews(query: string) {
14 | const response = await fetch(
15 | `https://api.search.brave.com/res/v1/news/search?q=${encodeURIComponent(
16 | query
17 | )}&count=5&search_lang=en&spellcheck=1`,
18 | {
19 | headers: {
20 | "Accept": "application/json",
21 | "Accept-Encoding": "gzip",
22 | "X-Subscription-Token": getEnv("BRAVE_SEARCH_API_KEY"),
23 | },
24 | }
25 | );
26 |
27 | if (!response.ok) {
28 | console.error("response error, retry");
29 | throw new Error("Failed to fetch news");
30 | }
31 |
32 | return response;
33 | }
34 |
35 |
36 | async function* searchWeb({ query }: SearchWebToolArgs) {
37 |
38 | let response: Response | undefined = undefined;
39 | response = await retry_if_fail(() => { return fetchNews(query) });
40 | if (!response || !response.ok) {
41 | throw new Error("Failed to fetch news");
42 | }
43 |
44 | const data = await response.json() as BraveSearchNewsRoot;
45 | const sites = data.results
46 | .map((r) => ({
47 | title: r.title,
48 | host: new URL(r.url).host,
49 | content: r.extra_snippets ? r.extra_snippets.join("\n") : undefined,
50 | }))
51 | .filter((s) => s.content);
52 |
53 | yield {
54 | done: sites,
55 | }
56 | }
57 |
58 | export const searchWebTool = makeTool({
59 | name: ToolName.searchWeb,
60 | zodObj: schema,
61 | function: searchWeb,
62 | });
63 |
--------------------------------------------------------------------------------
/src/shared/tools/utils.ts:
--------------------------------------------------------------------------------
1 | import { ChatCompletionTool } from "openai/resources/index.mjs"
2 | import { z } from "zod"
3 | import zodToJsonSchema from "zod-to-json-schema"
4 |
5 | export enum ToolName {
6 | createMarket = "createMarket",
7 | searchWeb = "searchWeb",
8 | searchImages = "searchImages",
9 | }
10 |
11 | export function makeTool, K>(props: {
12 | name: N,
13 | zodObj: T,
14 | description?: string,
15 | function: (args: z.infer, extraArgs: ExtraArgs) => K
16 | }): {
17 | name: N
18 | definition: ChatCompletionTool
19 | description?: string
20 | function: (args: any, extraArgs: ExtraArgs) => K
21 | } {
22 | const parameters = zodToJsonSchema(props.zodObj)
23 | return {
24 | name: props.name,
25 | description: props.description,
26 | definition: {
27 | "type": "function",
28 | "function": {
29 | "name": props.name,
30 | "description": props.description,
31 | parameters,
32 | }
33 | },
34 |
35 | function: props.function
36 | }
37 | }
--------------------------------------------------------------------------------
/src/types/brave_search_image.d.ts:
--------------------------------------------------------------------------------
1 | export { }
2 | declare global {
3 | export type BraveSearchImagesRoot = {
4 | type: string;
5 | query: {
6 | original: string;
7 | spellcheck_off: boolean;
8 | show_strict_warning: boolean;
9 | };
10 | results: Array<{
11 | type: string;
12 | title: string;
13 | url: string;
14 | source: string;
15 | page_fetched: string;
16 | thumbnail: {
17 | src: string;
18 | };
19 | properties: {
20 | url: string;
21 | placeholder: string;
22 | };
23 | meta_url: {
24 | scheme: string;
25 | netloc: string;
26 | hostname: string;
27 | favicon: string;
28 | path: string;
29 | };
30 | confidence: 'high' | 'medium' | 'low';
31 | }>;
32 | };
33 | }
--------------------------------------------------------------------------------
/src/types/brave_search_news.d.ts:
--------------------------------------------------------------------------------
1 |
2 | export { };
3 |
4 | declare global {
5 | type BraveSearchNewsRoot = {
6 | type: string;
7 | query: {
8 | original: string;
9 | spellcheck_off: boolean;
10 | show_strict_warning: boolean;
11 | };
12 | results: Array<{
13 | type: string;
14 | title: string;
15 | url: string;
16 | description: string;
17 | age: string;
18 | page_age: string;
19 | meta_url: {
20 | scheme: string;
21 | netloc: string;
22 | hostname: string;
23 | favicon: string;
24 | path: string;
25 | };
26 | thumbnail: {
27 | src: string;
28 | };
29 | extra_snippets: Array;
30 | }>;
31 | };
32 | }
--------------------------------------------------------------------------------
/src/types/brave_search_web.d.ts:
--------------------------------------------------------------------------------
1 |
2 | export { };
3 |
4 | declare global {
5 | type BraveSearchWebRoot = {
6 | query: {
7 | original: string;
8 | show_strict_warning: boolean;
9 | is_navigational: boolean;
10 | is_news_breaking: boolean;
11 | spellcheck_off: boolean;
12 | country: string;
13 | bad_results: boolean;
14 | should_fallback: boolean;
15 | postal_code: string;
16 | city: string;
17 | header_country: string;
18 | more_results_available: boolean;
19 | state: string;
20 | };
21 | mixed: {
22 | type: string;
23 | main: Array<{
24 | type: string;
25 | index?: number;
26 | all: boolean;
27 | }>;
28 | top: Array;
29 | side: Array;
30 | };
31 | type: string;
32 | videos: {
33 | type: string;
34 | results: Array<{
35 | type: string;
36 | url: string;
37 | title: string;
38 | description: string;
39 | video: {};
40 | meta_url: {
41 | scheme: string;
42 | netloc: string;
43 | hostname: string;
44 | favicon: string;
45 | path: string;
46 | };
47 | thumbnail: {
48 | src: string;
49 | original: string;
50 | };
51 | age?: string;
52 | page_age?: string;
53 | }>;
54 | mutated_by_goggles: boolean;
55 | };
56 | web: {
57 | type: string;
58 | results: Array<{
59 | title: string;
60 | url: string;
61 | is_source_local: boolean;
62 | is_source_both: boolean;
63 | description: string;
64 | page_age?: string;
65 | profile: {
66 | name: string;
67 | url: string;
68 | long_name: string;
69 | img: string;
70 | };
71 | language: string;
72 | family_friendly: boolean;
73 | type: string;
74 | subtype: string;
75 | is_live: boolean;
76 | meta_url: {
77 | scheme: string;
78 | netloc: string;
79 | hostname: string;
80 | favicon: string;
81 | path: string;
82 | };
83 | thumbnail?: {
84 | src: string;
85 | original: string;
86 | logo: boolean;
87 | };
88 | age?: string;
89 | extra_snippets?: Array;
90 | deep_results?: {
91 | buttons: Array<{
92 | type: string;
93 | title: string;
94 | url: string;
95 | }>;
96 | };
97 | }>;
98 | family_friendly: boolean;
99 | };
100 | };
101 |
102 | }
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | import { InstaQLEntity, InstaQLSubscriptionState } from "@instantdb/core";
5 | import { Cursor, InstaQLParams } from "@instantdb/core/dist/module/queryTypes";
6 | import { LineSeriesPartialOptions, UTCTimestamp } from "lightweight-charts";
7 | import { ChatCompletionChunk } from "openai/src/resources/index.js";
8 | import { AppSchema } from "~/../instant.schema";
9 | import { db } from "~/client/database";
10 | import { tools } from "~/shared/tools";
11 | import { ToolName } from "~/shared/tools/utils";
12 | import { buyShare, calcAttributes, sellShare } from "~/shared/utils";
13 |
14 | ///
15 |
16 | export { };
17 |
18 | declare global {
19 | // Utility types
20 | type Refine = Omit & { [P in K]: V };
21 |
22 | type OneOf = {
23 | [K in keyof T]: {
24 | [P in keyof T]?: P extends K ? T[P] : never;
25 | } & { [P in Exclude]?: undefined };
26 | }[keyof T];
27 |
28 | type ExtractType AsyncGenerator> =
29 | T extends (...args: any[]) => AsyncGenerator ? U extends Record ? D : never : never;
30 |
31 | type DeepNonNullable = T extends Function
32 | ? T
33 | : T extends Array
34 | ? Array>>
35 | : T extends object
36 | ? { [K in keyof T]-?: DeepNonNullable> }
37 | : T;
38 |
39 | type InstantDBQueryResponse> = Awaited>>;
40 |
41 | // API types
42 | type UpvoteDownvote = {
43 | type: 'upvote' | 'downvote' | 'remove'
44 | }
45 |
46 | type BuySellAction = {
47 | type: "buy" | "sell";
48 | amount: number;
49 | shareId: string;
50 | };
51 |
52 | type JWTResult = {
53 | type: "existing" | "new";
54 | profile_id: string;
55 | };
56 |
57 | type APICompleteBody = {
58 | blocks: Block[];
59 | };
60 |
61 | type YesOrNo = "yes" | "no";
62 |
63 |
64 |
65 | // Market related types
66 | type Ext_Option = ReturnType["options"][number];
67 |
68 | type BuySellProps = {
69 | market: {
70 | options: {
71 | normalizedYesProb: number | undefined;
72 | yesProb: number | undefined;
73 | id: string;
74 | color: string;
75 | name: string;
76 | image: string;
77 | shares: Share[];
78 | }[];
79 | id: string;
80 | name: string;
81 | image: string;
82 | description: string;
83 | };
84 | };
85 |
86 |
87 |
88 |
89 | // Now extract the type of the response using ReturnType and Awaited
90 | type MarketSocialQueryRespose = InstantDBQueryResponse<{
91 | markets: {
92 | $: {
93 | where: {
94 | id: string,
95 | },
96 | },
97 | votes: {
98 | $: {
99 | where: {
100 | profile: string,
101 | },
102 | },
103 | },
104 | },
105 | }>;
106 |
107 |
108 |
109 | type MarketResponse = InstantDBQueryResponse<{
110 | markets: {
111 | options: {
112 | shares: {},
113 | },
114 | $: {
115 | first: number,
116 | after: Cursor,
117 | order: {
118 | num_votes: "desc",
119 | },
120 | },
121 | },
122 | }>
123 |
124 | // Profile related types
125 | type ProfileResponse = DeepNonNullable>
141 |
142 | // Subscription types
143 | type ProfileSubscription = InstaQLSubscriptionState<
144 | AppSchema,
145 | {
146 | profiles: {
147 | $: {
148 | where: {
149 | id: string;
150 | };
151 | };
152 | holdings: {
153 | share: {};
154 | };
155 | };
156 | }
157 | >;
158 |
159 | type MarketSubscription = InstaQLSubscriptionState<
160 | AppSchema,
161 | {
162 | markets: {
163 | $: {
164 | where: {
165 | id: string;
166 | };
167 | };
168 | options: {
169 | shares: {};
170 |
171 | };
172 | };
173 | }
174 | >;
175 |
176 | type HoldingSubscription = InstaQLSubscriptionState<
177 | AppSchema,
178 | {
179 | holdings: {
180 | $: {
181 | where: {
182 | "profile.id": string;
183 | "share.id": {
184 | $in: string;
185 | };
186 | };
187 | };
188 | share: {};
189 | };
190 | }
191 | >;
192 |
193 | type HistoryOptionSubscription = InstaQLSubscriptionState<
194 | AppSchema,
195 | {
196 | history__options: {
197 | $: {
198 | limit: number;
199 | order: {
200 | serverCreatedAt: "desc";
201 | };
202 | where: {
203 | option_id: {
204 | $in: string[];
205 | };
206 | };
207 | };
208 | };
209 | }
210 | >;
211 |
212 |
213 | // Chat related types
214 | type ChatTaskMessage = OneOf<{
215 | chunk: ChatCompletionChunk
216 | }>;
217 |
218 |
219 |
220 | type Status = OneOf<{
221 | idle: {};
222 | doing: {};
223 | done_succ: K;
224 | done_err: {};
225 | }>;
226 |
227 | // Action result types
228 | type ShareActionResult_Buy = ReturnType;
229 | type ShareActionResult_Sell = ReturnType;
230 | type ShareActionResult = ShareActionResult_Buy | ShareActionResult_Sell;
231 |
232 | // Entity types
233 | type Profile = InstaQLEntity;
234 | type Holding = InstaQLEntity;
235 | type Share = InstaQLEntity;
236 | type Vote = InstaQLEntity;
237 | type Option = InstaQLEntity;
238 | type Market = InstaQLEntity;
239 | type HistoryOption = InstaQLEntity;
240 | type Conversation = InstaQLEntity;
241 |
242 | type AgentStep = 'reasoning_and_foward' | 'tool_call_and_content'
243 | // Block types
244 | type BaseBlock = InstaQLEntity & {
245 | content: unknown;
246 | } & {
247 | agent_step?: AgentStep,
248 | }
249 |
250 | type ToolBlock = Refine<
251 | BaseBlock & {
252 | role: "tool";
253 | },
254 | "content",
255 | {
256 | name: ToolName;
257 | arguments_partial_str: string;
258 | arguments?: T;
259 | result?: K;
260 | doings: D[]
261 | }
262 | >;
263 |
264 |
265 | type ReasoningBlock = Refine<
266 | BaseBlock & {
267 | role: "reasoning";
268 | },
269 | "content",
270 | string
271 | >;
272 |
273 |
274 | type AssistantBlock = Refine<
275 | BaseBlock & {
276 | role: "assistant";
277 | },
278 | "content",
279 | string
280 | >;
281 |
282 | type UserBlock = Refine<
283 | BaseBlock & {
284 | role: "user";
285 | },
286 | "content",
287 | string
288 | >;
289 |
290 | type Block = ToolBlock | AssistantBlock | UserBlock | ReasoningBlock;
291 |
292 | // UI types
293 | type UIBlock = Block & {
294 | isStartSecion: boolean;
295 | isEndSecion: boolean;
296 | };
297 |
298 | type Blocks = { [id: string]: Block };
299 | type DataPoint = { time: UTCTimestamp; value: number };
300 | type Series = {
301 | id: string;
302 | title: string;
303 | data: DataPoint[];
304 | options: LineSeriesPartialOptions;
305 | };
306 |
307 | type ToolYieldWithId = ToolYield & {
308 | id: string
309 | }
310 | type ChatStreamYield = NonNullable>
316 | & { agent_step: AgentStep }
317 |
318 | type UnwrapAsyncGenerator = T extends AsyncGenerator
319 | ? Y
320 | : never;
321 |
322 | type _ToolYield = (typeof tools)[0];
323 | type ToolYield = T extends any
324 | ? { name: T['name'] } & UnwrapAsyncGenerator>
325 | : never;
326 |
327 | type Update = OneOf<{
328 | reasoning: OneOf<{
329 | started: {
330 | created_at: string,
331 | id: string
332 | text: string
333 | }
334 |
335 | delta: {
336 | created_at: string,
337 | updated_at: string,
338 | id: string
339 | text: string
340 | }
341 |
342 | done: {
343 | created_at: string,
344 | updated_at: string,
345 | id: string
346 | content: string
347 | }
348 | }>,
349 |
350 | tool: OneOf<{
351 | started: {
352 | created_at: string,
353 | name: ToolName,
354 | id: string,
355 | },
356 | delta: {
357 | created_at: string,
358 | updated_at: string,
359 | name: ToolName,
360 | id: string,
361 | arguments_delta: string
362 | }
363 | done: {
364 | created_at: string,
365 | updated_at: string,
366 | name: ToolName,
367 | id: string
368 | arguments: unknown
369 | }
370 | }>
371 |
372 | content: OneOf<{
373 | started: {
374 | created_at: string,
375 | id: string
376 | text: string
377 | }
378 |
379 | delta: {
380 | created_at: string,
381 | updated_at: string,
382 | id: string
383 | text: string
384 | }
385 |
386 | done: {
387 | created_at: string,
388 | updated_at: string,
389 | id: string
390 | content: string
391 | }
392 | }>
393 | }>
394 |
395 | type HighLevelMessage = OneOf<{
396 | doing: Update,
397 | done: {
398 | tool_called: boolean
399 | }
400 | }>
401 |
402 | type ToolRecord = {
403 | name: ToolName;
404 | created_at: string;
405 | arguments_str: string;
406 | }
407 |
408 | type ContentRecord = {
409 | content: string;
410 | created_at?: string;
411 | id: string;
412 | }
413 |
414 |
415 | type ReasoningRecord = {
416 | content: string;
417 | created_at?: string;
418 | id: string;
419 | }
420 |
421 | type MemStorage = {
422 | [tool_call_id: string]: ToolYieldWithId[]
423 | }
424 |
425 | type ExtraArgs = { memStorage: MemStorage }
426 | }
427 |
--------------------------------------------------------------------------------
/src/types/window.d.ts:
--------------------------------------------------------------------------------
1 | interface Window {
2 | env: {
3 | [key: string]: string | undefined;
4 | };
5 | }
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
4 | theme: {
5 | extend: {
6 | transitionProperty: {
7 | 'width': 'width'
8 | },
9 | animation: {
10 | 'fade-out': 'fadeOut 0.5s ease-in-out',
11 | 'fade-in': 'fadeIn 0.5s ease-in-out'
12 | },
13 | keyframes: {
14 | fadeOut: {
15 | '0%': { opacity: '1' },
16 | '100%': { opacity: '0' }
17 | },
18 | fadeIn: {
19 | '0%': { opacity: '0' },
20 | '100%': { opacity: '1' }
21 | }
22 | }
23 | }
24 | },
25 | plugins: [
26 | function ({ addUtilities }) {
27 | addUtilities({
28 | '.animate-paused': {
29 | 'animation-play-state': 'paused!important',
30 | },
31 | }, ['responsive', 'hover']);
32 | },
33 | require('@tailwindcss/typography'),
34 | ]
35 | };
36 |
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "allowSyntheticDefaultImports": true,
7 | "esModuleInterop": true,
8 | "jsx": "preserve",
9 | "jsxImportSource": "solid-js",
10 | "allowJs": true,
11 | "noEmit": true,
12 | "strict": true,
13 | "types": ["vinxi/types/client"],
14 | "isolatedModules": true,
15 | "paths": {
16 | "~/*": ["./src/*"]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------