├── .gitignore ├── README.md ├── agent-service ├── .dockerignore ├── .env.example ├── Dockerfile.dev ├── Procfile ├── README.md ├── app │ ├── __init__.py │ ├── agent.py │ ├── configuration.py │ ├── graph_image.png │ ├── image.py │ ├── main.py │ └── prompts.py ├── compose.yml ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── tests │ └── __init__.py ├── agent-ui ├── .env.example ├── .gitignore ├── README.md ├── app │ ├── (base) │ │ ├── (auth) │ │ │ ├── layout.tsx │ │ │ ├── sign-in │ │ │ │ └── [[...sign-in]] │ │ │ │ │ └── page.tsx │ │ │ └── sign-up │ │ │ │ └── [[...sign-up]] │ │ │ │ └── page.tsx │ │ ├── drafts │ │ │ ├── delete-draft.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── new │ │ │ └── page.tsx │ ├── (chat) │ │ ├── actions.tsx │ │ ├── chat │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── actions.ts │ ├── globals.css │ ├── layout.tsx │ ├── opengraph-image.png │ └── twitter-image.png ├── components.json ├── components │ ├── button-scroll-to-bottom.tsx │ ├── chat-history.tsx │ ├── chat-list.tsx │ ├── chat-message-actions.tsx │ ├── chat-message.tsx │ ├── chat-panel.tsx │ ├── chat-share-dialog.tsx │ ├── chat-with-artifacts.tsx │ ├── chat.tsx │ ├── clear-history.tsx │ ├── content-document.tsx │ ├── contexts │ │ └── artifact-context.tsx │ ├── empty-screen.tsx │ ├── external-link.tsx │ ├── footer.tsx │ ├── generate-post.tsx │ ├── header.tsx │ ├── loading-spinner-message.tsx │ ├── login-button.tsx │ ├── markdown.tsx │ ├── message.tsx │ ├── post-draft.tsx │ ├── post-generator-form.tsx │ ├── prompt-form.tsx │ ├── providers.tsx │ ├── save-draft.tsx │ ├── sidebar-actions.tsx │ ├── sidebar-desktop.tsx │ ├── sidebar-footer.tsx │ ├── sidebar-item.tsx │ ├── sidebar-items.tsx │ ├── sidebar-list.tsx │ ├── sidebar-mobile.tsx │ ├── sidebar-toggle.tsx │ ├── sidebar.tsx │ ├── socialmedia-post.tsx │ ├── spinner.tsx │ ├── tailwind-indicator.tsx │ ├── theme-toggle.tsx │ ├── ui │ │ ├── alert-dialog.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── codeblock.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── icons.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx │ └── user-menu.tsx ├── lib │ ├── constants.ts │ ├── hooks │ │ ├── use-copy-to-clipboard.tsx │ │ ├── use-enter-submit.tsx │ │ ├── use-local-storage.ts │ │ ├── use-scroll-anchor.tsx │ │ ├── use-sidebar.tsx │ │ └── use-streamable-text.ts │ ├── redis.ts │ ├── types.ts │ └── utils.ts ├── middleware.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.cjs ├── public │ ├── bot.svg │ ├── delete.svg │ ├── dots-loading.svg │ ├── dynamic-man.gif │ ├── favicon.ico │ ├── home.svg │ ├── library.svg │ ├── linkedin.svg │ ├── logo.svg │ ├── person.svg │ ├── plus.svg │ ├── save.svg │ ├── send.svg │ └── twitter.svg ├── tailwind.config.ts ├── tsconfig.json └── vercel.json └── agentui-action.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | *.manifest 27 | *.spec 28 | 29 | # Installer logs 30 | pip-log.txt 31 | pip-delete-this-directory.txt 32 | 33 | # Unit test / coverage reports 34 | htmlcov/ 35 | .tox/ 36 | .nox/ 37 | .coverage 38 | .coverage.* 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | *.cover 43 | *.py,cover 44 | .hypothesis/ 45 | .pytest_cache/ 46 | .coverage.* 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | db.sqlite3 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # Jupyter Notebook 68 | .ipynb_checkpoints 69 | 70 | # IPython 71 | profile_default/ 72 | ipython_config.py 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # pipenv 78 | pipenv 79 | pipfile.lock 80 | 81 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 82 | __pypackages__/ 83 | 84 | # Celery stuff 85 | celerybeat-schedule 86 | celerybeat.pid 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .env. 94 | .venv 95 | venv/ 96 | ENV/ 97 | env/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # pytype static type analyzer 120 | .pytype/ 121 | 122 | # profiling data 123 | .prof 124 | 125 | # macOS 126 | .DS_Store 127 | 128 | # Docker 129 | Dockerfile 130 | docker-compose.yml 131 | 132 | # Node 133 | node_modules/ 134 | npm-debug.log 135 | yarn-debug.log 136 | yarn-error.log 137 | 138 | # Editor directories and files 139 | .idea/ 140 | .vscode/ 141 | *.sublime-project 142 | *.sublime-workspace"agent-service-v2/app/travel2.backup.sqlite" 143 | "agent-service-v2/app/travel2.sqlite" 144 | "agent-service-v2/travel2.backup.sqlite" 145 | "agent-service-v2/travel2.sqlite" 146 | *.sqlite 147 | *.backup.sqlite -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostBot 3000 AI Agent 2 | 3 | Try it out: [PostBot 3000](https://postbot3000.vercel.app/) 4 | 5 | PostBot 3000 is an `open-source` project that shows how to build a powerful AI agent and stream responses and generate artifacts. This project makes it easier for anyone looking to implement similar solutions. 6 | 7 | ## Features 8 | 9 | 1. **OpenSource:** All the code (`agent-ui` & `agent-service`) is open-source and available on GitHub. 10 | 2. **Artifacts:** The posts content generated by agent is streamed in Artifacts (like `Claude`). 11 | 3. **Agent Python Backend:** The agent is built using `LangGraph` `Python`, for AI workflows and FastAPI for creating a robust API. 12 | 13 | ### Agent Workflow 14 | 15 | ![agent](./agent-service//app/graph_image.png) 16 | 17 | ### Agent In Action 18 | 19 | ![AgentUI Action](agentui-action.png) 20 | 21 | ## Tech Stack 22 | 23 | - [LangGraph](https://langchain-ai.github.io/langgraph) 24 | - [Vercel AI SDK](https://sdk.vercel.ai/docs/introduction) 25 | - [gpt-4o-mini](https://platform.openai.com/docs/models/gpt-4o-mini) 26 | - [FastAPI](https://fastapi.tiangolo.com) 27 | - [Next.js](https://nextjs.org/) 28 | - [TailwindCSS](https://tailwindcss.com) 29 | - [Clerk Auth](https://clerk.com) 30 | - [Upstash Redis](https://upstash.com) 31 | 32 | ## Try it Out 33 | 34 | 1. Clone the repository 35 | 36 | ```bash 37 | git clone https://github.com/ahmad2b/postbot3000.git 38 | ``` 39 | 40 | 2. Create a `.env` file based on the `.env.example` file and add your API in both `agent-service` and `agent-ui` directory. 41 | 42 | 3. Install the dependencies for `agent-service`: 43 | 44 | ```bash 45 | cd agent-service 46 | poetry install 47 | ``` 48 | 49 | 4. Run the development server for `agent-service`: 50 | 51 | ```bash 52 | poetry run uvicorn app.main:app --reload 53 | ``` 54 | 55 | 5. Install the dependencies for `agent-ui`: 56 | 57 | ```bash 58 | cd agent-ui 59 | npm install 60 | # or 61 | yarn install 62 | # or 63 | pnpm install 64 | ``` 65 | 66 | 6. Run the development server for `agent-ui`: 67 | 68 | ```bash 69 | npm run dev 70 | # or 71 | yarn dev 72 | # or 73 | pnpm dev 74 | ``` 75 | 76 | 7. Open http://localhost:3000 with your browser to see the result. 77 | 78 | --- 79 | 80 | ## Community & Connect 81 | 82 | I'm excited to see this project helping developers build better AI agents! If you're exploring AI agents or building something exciting, I'd love to hear about your journey. 83 | 84 | Find me around the web: 85 | 86 | - 📧 Email: ahmadshaukat_4@outlook.com 87 | - 📍 LinkedIn: [ahmad2b](https://www.linkedin.com/in/ahmad2b) 88 | - 🐦 Twitter: [@mahmad2b](https://x.com/mahmad2b) 89 | 90 | If this project helped your development journey, consider giving it a ⭐ to help others find it! 91 | 92 | --- 93 | 94 | #### Inspiration 95 | 96 | - Agent UI: [Vercel Template](https://vercel.com/templates/next.js/nextjs-ai-chatbot) 97 | - Agent Service: [MLExpert by Venelin Valkov](https://www.mlexpert.io/bootcamp/write-social-media-content-with-agents) 98 | -------------------------------------------------------------------------------- /agent-service/.dockerignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | *.py,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | .coverage.* 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | build/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # IPython 75 | profile_default/ 76 | ipython_config.py 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # pipenv 82 | pipenv 83 | pipfile.lock 84 | 85 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 86 | __pypackages__/ 87 | 88 | # Celery stuff 89 | celerybeat-schedule 90 | celerybeat.pid 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .env.* 98 | .venv 99 | venv/ 100 | ENV/ 101 | env/ 102 | env.bak/ 103 | venv.bak/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | .spyproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | .dmypy.json 118 | dmypy.json 119 | 120 | # Pyre type checker 121 | .pyre/ 122 | 123 | # pytype static type analyzer 124 | .pytype/ 125 | 126 | # profiling data 127 | .prof 128 | 129 | # macOS 130 | .DS_Store 131 | 132 | # Docker 133 | Dockerfile 134 | docker-compose.yml 135 | 136 | # Node 137 | node_modules/ 138 | npm-debug.log 139 | yarn-debug.log 140 | yarn-error.log 141 | 142 | # Editor directories and files 143 | .idea/ 144 | .vscode/ 145 | *.sublime-project 146 | *.sublime-workspace 147 | -------------------------------------------------------------------------------- /agent-service/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=XXXXXXXX 2 | 3 | LANGCHAIN_API_KEY=XXXXXXXX 4 | LANGCHAIN_TRACING_V2=true 5 | LANGCHAIN_PROJECT="postbot3000" -------------------------------------------------------------------------------- /agent-service/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | 3 | LABEL maintainer=["Muhammad Ahmad "] 4 | 5 | WORKDIR /code 6 | RUN apt-get update && apt-get install -y \ 7 | build-essential \ 8 | libpq-dev \ 9 | portaudio19-dev \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | RUN pip install poetry 13 | COPY . /code/ 14 | RUN poetry config virtualenvs.create false 15 | RUN poetry install 16 | EXPOSE 8000 17 | 18 | CMD ["sh", "-c", "poetry run uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000} --reload"] -------------------------------------------------------------------------------- /agent-service/Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn app.main:app --host=0.0.0.0 --port=${PORT} 2 | -------------------------------------------------------------------------------- /agent-service/README.md: -------------------------------------------------------------------------------- 1 | # PostBot 3000 - Agent Service 2 | -------------------------------------------------------------------------------- /agent-service/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmad2b/postbot3000/a77e5dcf19a107fa49e38e32ee77e1490e3d82cf/agent-service/app/__init__.py -------------------------------------------------------------------------------- /agent-service/app/agent.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dotenv import load_dotenv 3 | from typing import Optional, TypedDict 4 | from pydantic import BaseModel 5 | 6 | from langchain_core.messages import HumanMessage, SystemMessage 7 | from langchain_openai import ChatOpenAI 8 | from langgraph.graph import END, StateGraph, START 9 | 10 | from app.configuration import Configuration 11 | from app.prompts import (EDITOR_PROMPT, TWITTER_PROMPT, LINKEDIN_PROMPT, TWTTTER_CRITIQUE_PROMPT, LINKEDIN_CRITIQUE_PROMPT) 12 | 13 | # Configure logging 14 | logging.basicConfig(level=logging.INFO) 15 | logger = logging.getLogger(__name__) 16 | 17 | load_dotenv() 18 | 19 | llm = ChatOpenAI( 20 | model="gpt-4o-mini", 21 | temperature=0, 22 | ) 23 | 24 | 25 | # Define the State for the Agent 26 | class Post(BaseModel): 27 | """A post written in a different versions""" 28 | 29 | drafts: list[str] 30 | feedback: Optional[str] 31 | 32 | class OverallState(TypedDict): 33 | user_text: str 34 | target_audience: str 35 | edit_text: str 36 | tweet: Post 37 | linkedin_post: Post 38 | n_drafts: int 39 | workflow_status: str 40 | 41 | # Define the nodes 42 | def editor_node(state: OverallState): 43 | logger.info("Entering editor_node with state: %s", state) 44 | prompt = f""" 45 | text: 46 | ``` 47 | {state["user_text"]} 48 | ``` 49 | """.strip() 50 | 51 | response = llm.invoke([SystemMessage(EDITOR_PROMPT), HumanMessage(prompt)]) 52 | logger.info("editor_node response: %s", response.content) 53 | 54 | return {"edit_text": response.content} 55 | 56 | def tweet_writer_node(state: OverallState): 57 | logger.info("Entering tweet_writer_node with state: %s", state) 58 | post = state["tweet"] 59 | feedback_prompt = ( 60 | "" 61 | if not post["feedback"] 62 | else f""" 63 | Tweet: 64 | ``` 65 | {post["drafts"][-1]} 66 | ``` 67 | """.strip() 68 | ) 69 | 70 | prompt = f""" 71 | text: 72 | ``` 73 | {state["edit_text"]} 74 | ``` 75 | 76 | {feedback_prompt} 77 | 78 | Target audience: {state["target_audience"]} 79 | 80 | Write only the text for the post 81 | """.strip() 82 | 83 | response = llm.invoke([SystemMessage(TWITTER_PROMPT), HumanMessage(prompt)]) 84 | logger.info("tweet_writer_node response: %s", response.content) 85 | post["drafts"].append(response.content) 86 | return {"tweet": post} 87 | 88 | def linkedin_writer_node(state: OverallState): 89 | logger.info("Entering linkedin_writer_node with state: %s", state) 90 | post = state["linkedin_post"] 91 | feedback_prompt = ( 92 | "" 93 | if not post["feedback"] 94 | else f""" 95 | LinkedIn post: 96 | ``` 97 | {post["drafts"][-1]} 98 | ``` 99 | 100 | Use the feedback to improve it: 101 | ``` 102 | {post["feedback"]} 103 | ``` 104 | """.strip() 105 | ) 106 | 107 | prompt = f""" 108 | text: 109 | ``` 110 | {state["edit_text"]} 111 | ``` 112 | 113 | {feedback_prompt} 114 | 115 | Target audience: {state["target_audience"]} 116 | 117 | Write only the text for the post 118 | """.strip() 119 | 120 | response = llm.invoke([SystemMessage(LINKEDIN_PROMPT), HumanMessage(prompt)]) 121 | logger.info("linkedin_writer_node response: %s", response.content) 122 | post["drafts"].append(response.content) 123 | return {"linkedin_post": post} 124 | 125 | def critique_tweet_node(state: OverallState): 126 | logger.info("Entering critique_tweet_node with state: %s", state) 127 | post = state["tweet"] 128 | prompt = f""" 129 | Full post: 130 | ``` 131 | {state["edit_text"]} 132 | ``` 133 | 134 | Suggested tweet (critique this): 135 | ``` 136 | {post["drafts"][-1]} 137 | ``` 138 | 139 | Target audience: {state["target_audience"]} 140 | """.strip() 141 | 142 | response = llm.invoke([SystemMessage(TWTTTER_CRITIQUE_PROMPT), HumanMessage(prompt)]) 143 | logger.info("critique_tweet_node response: %s", response.content) 144 | post["feedback"] = response.content 145 | return {"tweet": post} 146 | 147 | def critique_linkedin_node(state: OverallState): 148 | logger.info("Entering critique_linkedin_node with state: %s", state) 149 | post = state["linkedin_post"] 150 | prompt = f""" 151 | Full post: 152 | ``` 153 | {state["edit_text"]} 154 | ``` 155 | 156 | Suggested LinkedIn post (critique this): 157 | ``` 158 | {post["drafts"][-1]} 159 | ``` 160 | 161 | Target audience: {state["target_audience"]} 162 | """.strip() 163 | 164 | response = llm.invoke([SystemMessage(LINKEDIN_CRITIQUE_PROMPT), HumanMessage(prompt)]) 165 | logger.info("critique_linkedin_node response: %s", response.content) 166 | post["feedback"] = response.content 167 | return {"linkedin_post": post} 168 | 169 | def supervisor_node(state: OverallState): 170 | logger.info("Entering supervisor_node with state: %s", state) 171 | if len(state["tweet"]["drafts"]) >= state["n_drafts"] and len(state["linkedin_post"]["drafts"]) >= state["n_drafts"]: 172 | state["workflow_status"] = "completed" 173 | else: 174 | state["workflow_status"] = "in_progress" 175 | return state 176 | 177 | # Define the Edges 178 | def should_rewrite(state: OverallState): 179 | tweet = state["tweet"] 180 | linked_post = state["linkedin_post"] 181 | n_drafts = state["n_drafts"] 182 | if len(tweet["drafts"]) >= n_drafts and len(linked_post["drafts"]) >= n_drafts: 183 | logger.info("should_rewrite decision: END") 184 | return END 185 | 186 | logger.info("should_rewrite decision: ['linkedin_critique', 'tweet_critique']") 187 | return ["linkedin_critique", "tweet_critique"] 188 | 189 | # Build a Graph 190 | workflow = StateGraph(OverallState, config_schema=Configuration) 191 | 192 | workflow.add_node("editor", editor_node) 193 | workflow.add_node("tweet_writer", tweet_writer_node) 194 | workflow.add_node("tweet_critique", critique_tweet_node) 195 | workflow.add_node("linkedin_writer", linkedin_writer_node) 196 | workflow.add_node("linkedin_critique", critique_linkedin_node) 197 | workflow.add_node("supervisor", supervisor_node) 198 | 199 | workflow.add_edge("editor", "tweet_writer") 200 | workflow.add_edge("editor", "linkedin_writer") 201 | 202 | workflow.add_edge("tweet_writer", "supervisor") 203 | workflow.add_edge("linkedin_writer", "supervisor") 204 | workflow.add_conditional_edges("supervisor", should_rewrite) 205 | 206 | workflow.add_edge("tweet_critique", "tweet_writer") 207 | workflow.add_edge("linkedin_critique", "linkedin_writer") 208 | 209 | workflow.add_edge(START, "editor") 210 | 211 | graph = workflow.compile() 212 | -------------------------------------------------------------------------------- /agent-service/app/configuration.py: -------------------------------------------------------------------------------- 1 | """Define the configurable parameters for the agent.""" 2 | 3 | import os 4 | from dataclasses import dataclass, fields 5 | from typing import Any, Optional 6 | 7 | from langchain_core.runnables import RunnableConfig 8 | 9 | 10 | @dataclass(kw_only=True) 11 | class Configuration: 12 | """Main configuration class for the memory graph system.""" 13 | 14 | thread_id: str 15 | """The thread ID for the conversation.""" 16 | 17 | @classmethod 18 | def from_runnable_config( 19 | cls, config: Optional[RunnableConfig] = None 20 | ) -> "Configuration": 21 | """Create a Configuration instance from a RunnableConfig.""" 22 | configurable = ( 23 | config["configurable"] if config and "configurable" in config else {} 24 | ) 25 | values: dict[str, Any] = { 26 | f.name: os.environ.get(f.name.upper(), configurable.get(f.name)) 27 | for f in fields(cls) 28 | if f.init 29 | } 30 | 31 | return cls(**{k: v for k, v in values.items() if v}) -------------------------------------------------------------------------------- /agent-service/app/graph_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmad2b/postbot3000/a77e5dcf19a107fa49e38e32ee77e1490e3d82cf/agent-service/app/graph_image.png -------------------------------------------------------------------------------- /agent-service/app/image.py: -------------------------------------------------------------------------------- 1 | from app.agent import graph 2 | 3 | try: 4 | # Generate the image 5 | image_data = graph.get_graph(xray=True).draw_mermaid_png() 6 | 7 | name = "graph_image.png" 8 | 9 | # Save the image to a file 10 | with open(name, "wb") as image_file: 11 | image_file.write(image_data) 12 | print("Image saved as " + name) 13 | except Exception as e: 14 | # This requires some extra dependencies and is optional 15 | print(f"An error occurred: {e}") -------------------------------------------------------------------------------- /agent-service/app/main.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | import warnings 3 | from dotenv import load_dotenv 4 | import json 5 | 6 | from fastapi import FastAPI, Request, Response 7 | from langchain_core._api import LangChainBetaWarning 8 | from langserve import add_routes 9 | from fastapi.responses import StreamingResponse 10 | 11 | from app.agent import graph as agent_graph 12 | 13 | load_dotenv() 14 | 15 | warnings.filterwarnings("ignore", category=LangChainBetaWarning) 16 | 17 | @asynccontextmanager 18 | async def lifespan(app: FastAPI): 19 | yield 20 | 21 | app = FastAPI( 22 | title="PostBot 3000 - Agent Service", 23 | version="0.0.1", 24 | lifespan=lifespan 25 | ) 26 | 27 | @app.get("/health") 28 | async def health_check(): 29 | return {"status": "ok"} 30 | 31 | 32 | 33 | add_routes( 34 | app, 35 | agent_graph, 36 | disabled_endpoints=["stream_log", "batch", "config_hashes", "config_schema", "input_schema", "output_schema", "feedback"], 37 | ) 38 | -------------------------------------------------------------------------------- /agent-service/app/prompts.py: -------------------------------------------------------------------------------- 1 | """Define the Agent's prompts.""" 2 | 3 | 4 | EDITOR_PROMPT = """Rewrite for maximum social media engagement: 5 | 6 | - Use attention-grabbing, concise language 7 | - Inject personality and humor 8 | - Optimize formatting (short paragraphs) 9 | - Encourage interaction (questions, calls-to-action) 10 | - Ensure perfect grammar and spelling 11 | - Rewrite from first person perspective, when talking to an audience 12 | 13 | Use only the infromation provided in the text. Think carefull. 14 | """ 15 | 16 | TWITTER_PROMPT = """Generate a high-engagement tweet from the given text: 17 | 1. What problem does this solve? 18 | 2. Focus on the main technical points/features 19 | 3. Write a short, coherent paragraph (2-3 sentences max) 20 | 4. Use natural, conversational language 21 | 5. Optimize for virality: make it intriguing, relatable, or controversial 22 | 6. Exclude emojis and hashtags 23 | """ 24 | 25 | TWTTTER_CRITIQUE_PROMPT = """You are a Tweet Critique Agent. Your task is to analyze tweets and provide actionable feedback to make them more engaging. Focus on: 26 | 27 | 1. Clarity: Is the message clear and easy to understand? 28 | 2. Hook: Does it grapb attention in the first few words? 29 | 3. Brevity: Is it concise while maintaining impact? 30 | 4. Call-to-Action: Does it encourage interaction or sharing? 31 | 5. Tone: Is it appropriate for the intended audience? 32 | 6. Storytelling: Does it evoke curiosity? 33 | 7. Remove hype: Does it promise more than it delivers? 34 | 35 | Provide 2-3 specific suggestions to improve the tweet's engagement potential. 36 | Do not suggest hashtags. Keep your feedback concise and actionable. 37 | 38 | Your goal is to help the writer improve their social media writing skills and increase engagement with their posts. 39 | """ 40 | 41 | LINKEDIN_PROMPT = """Write a compelling LinkedIn post from the given text. Structure it as follows: 42 | 43 | 1. Eye-catching headline (5-7 words) 44 | 2. Identify a key problem or challenge 45 | 3. Provide a bullet list of key benefits/features 46 | 4. Highlight a clear benefit or solution 47 | 5. Conclude with a thought-provoking question 48 | 49 | Maintain a professional, informative tone. Avoid emojis and hashtags. 50 | Keep the post concise (50-80 words) and relevant to the industry. 51 | Focus on providing valuable insights or actionable takeaways that will resonate 52 | with professionals in the field. 53 | """ 54 | 55 | LINKEDIN_CRITIQUE_PROMPT = """ 56 | Your role is to analyze LinkedIn posts and provide actionable feedback to make them more engaging. 57 | Focus on the following aspects: 58 | 59 | 1. Hook: Evaluate the opening line’s ability to grab attention. 60 | 2. Structure: Assess the post’s flow and readability. 61 | 3. Content value: Determine if the post provides useful information or insights. 62 | 4. Call-to-action: Check if there’s a clear next step for readers. 63 | 5. Language: Suggest improvements in tone, style, and word choice. 64 | 6. Visual elements: Recommend additions or changes to images, videos, or formatting. 65 | 66 | For each aspect, provide: 67 | – A brief assessment (1–2 sentences) 68 | – A specific suggestion for improvement 69 | – A concise example of the suggested change 70 | 71 | Conclude with an overall recommendation for the most impactful change the author can make to increase engagement. 72 | Your goal is to help the writer improve their social media writing skills and increase engagement with their posts. 73 | """ -------------------------------------------------------------------------------- /agent-service/compose.yml: -------------------------------------------------------------------------------- 1 | name: postbot3000 2 | services: 3 | postbot3000: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile.dev 7 | volumes: 8 | - .:/code 9 | ports: 10 | - "8000:8000" 11 | networks: 12 | - postbot3000_network 13 | 14 | volumes: 15 | postbot3000: 16 | driver: local 17 | 18 | networks: 19 | postbot3000_network: 20 | driver: bridge 21 | -------------------------------------------------------------------------------- /agent-service/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Muhammad Ahmad "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.12" 10 | fastapi = "^0.115.0" 11 | uvicorn = {extras = ["standard"], version = "^0.30.3"} 12 | langchain = "^0.3.1" 13 | langchain-core = "^0.3.6" 14 | openai = "^1.50.2" 15 | langchain-community = "^0.3.1" 16 | langgraph = "^0.2.28" 17 | langserve = {extras = ["all"], version = "^0.3.0"} 18 | pydantic = "^2.9.2" 19 | langchain-openai = "^0.2.1" 20 | langchain-groq = "^0.2.0" 21 | langchain-google-genai = "^2.0.0" 22 | numexpr = "^2.10.1" 23 | langgraph-checkpoint = "^2.0.0" 24 | duckduckgo-search = "^6.2.13" 25 | pyowm = "^3.3.0" 26 | httpx = "^0.27.2" 27 | langsmith = "^0.1.129" 28 | python-dotenv = "^1.0.1" 29 | setuptools = "^75.1.0" 30 | tavily-python = "^0.5.0" 31 | pandas = "^2.2.3" 32 | ipython = "^8.28.0" 33 | pytest = "^8.3.3" 34 | psycopg = "^3.2.3" 35 | psycopg-pool = "^3.2.3" 36 | langgraph-checkpoint-postgres = "^2.0.1" 37 | redis = "^5.1.1" 38 | google-generativeai = "^0.8.3" 39 | matplotlib = "^3.9.2" 40 | 41 | 42 | [build-system] 43 | requires = ["poetry-core"] 44 | build-backend = "poetry.core.masonry.api" 45 | -------------------------------------------------------------------------------- /agent-service/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmad2b/postbot3000/a77e5dcf19a107fa49e38e32ee77e1490e3d82cf/agent-service/tests/__init__.py -------------------------------------------------------------------------------- /agent-ui/.env.example: -------------------------------------------------------------------------------- 1 | UPSTASH_REDIS_REST_URL=XXXXXXXX 2 | UPSTASH_REDIS_REST_TOKEN=XXXXXXXX 3 | 4 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=XXXXXXXX 5 | CLERK_SECRET_KEY=XXXXXXXX 6 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 7 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 8 | 9 | AGENT_URL=XXXXXXXX -------------------------------------------------------------------------------- /agent-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .env 36 | .vercel 37 | .vscode 38 | .env 39 | -------------------------------------------------------------------------------- /agent-ui/README.md: -------------------------------------------------------------------------------- 1 | # PostBot 3000 - Agent UI 2 | -------------------------------------------------------------------------------- /agent-ui/app/(base)/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const AuthLayout = ({ 4 | children 5 | }: Readonly<{ 6 | children: React.ReactNode 7 | }>) => { 8 | return ( 9 |
{children}
10 | ) 11 | } 12 | 13 | export default AuthLayout 14 | -------------------------------------------------------------------------------- /agent-ui/app/(base)/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs' 2 | 3 | export default function Page() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /agent-ui/app/(base)/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs' 2 | 3 | export default function Page() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /agent-ui/app/(base)/drafts/delete-draft.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { removePost } from '@/app/actions' 3 | import { Button } from '@/components/ui/button' 4 | import { useRouter } from 'next/navigation' 5 | import { useState } from 'react' 6 | import { toast } from 'sonner' 7 | 8 | interface DeleteDraftButtonProps { 9 | userId: string 10 | draftId: string 11 | } 12 | 13 | export const DeleteDraftButton = ({ 14 | draftId, 15 | userId 16 | }: DeleteDraftButtonProps) => { 17 | const [isLoading, setIsLoading] = useState(false) 18 | const router = useRouter() 19 | 20 | return ( 21 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /agent-ui/app/(base)/drafts/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const DraftsLayout = ({ children }: { children: React.ReactNode }) => { 4 | return
{children}
5 | } 6 | 7 | export default DraftsLayout 8 | -------------------------------------------------------------------------------- /agent-ui/app/(base)/drafts/page.tsx: -------------------------------------------------------------------------------- 1 | import { getPostsByUser } from '@/app/actions' 2 | import { Button, buttonVariants } from '@/components/ui/button' 3 | import { 4 | Card, 5 | CardContent, 6 | CardFooter, 7 | CardHeader, 8 | CardTitle 9 | } from '@/components/ui/card' 10 | import { cn } from '@/lib/utils' 11 | import { auth } from '@clerk/nextjs/server' 12 | import Image from 'next/image' 13 | import Link from 'next/link' 14 | import { redirect } from 'next/navigation' 15 | import { DeleteDraftButton } from './delete-draft' 16 | 17 | const DraftsPage = async () => { 18 | const { userId } = auth() 19 | 20 | if (!userId) { 21 | redirect('/') 22 | } 23 | 24 | const response = await getPostsByUser(userId) 25 | 26 | if (response.length === 0) { 27 | redirect('/') 28 | } 29 | 30 | console.log(response) 31 | 32 | return ( 33 |
34 |
35 | {' '} 36 |

37 | Saved Drafts 38 |

39 |
40 |
41 | {response.map((post, idx) => ( 42 | 43 | 44 | 45 |
48 | 60 |
61 |
62 |
63 | 64 |

{post.draft}

65 |
66 | 67 |
68 | 69 | 73 | View 74 | 75 |
76 |
77 |
78 | ))} 79 |
80 |
81 | ) 82 | } 83 | 84 | export default DraftsPage 85 | -------------------------------------------------------------------------------- /agent-ui/app/(base)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '@/components/header' 2 | import React from 'react' 3 | 4 | const BaseLayout = ({ children }: { children: React.ReactNode }) => { 5 | return ( 6 |
7 |
8 |
{children}
9 |
10 | ) 11 | } 12 | 13 | export default BaseLayout 14 | -------------------------------------------------------------------------------- /agent-ui/app/(base)/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default async function NewPage() { 4 | redirect('/') 5 | } 6 | -------------------------------------------------------------------------------- /agent-ui/app/(chat)/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from 'next/navigation' 2 | 3 | import { AI } from '@/app/(chat)/actions' 4 | import { getChat, getMissingKeys } from '@/app/actions' 5 | import { ChatWithArtifacts } from '@/components/chat-with-artifacts' 6 | import { auth } from '@clerk/nextjs/server' 7 | 8 | export interface ChatPageProps { 9 | params: { 10 | id: string 11 | } 12 | } 13 | 14 | export default async function ChatPage({ params }: ChatPageProps) { 15 | const { userId } = auth() 16 | const missingKeys = await getMissingKeys() 17 | 18 | if (!userId) { 19 | redirect(`/login?next=/chat/${params.id}`) 20 | } 21 | 22 | const chat = await getChat(params.id, userId) 23 | 24 | if (!chat || 'error' in chat) { 25 | redirect('/') 26 | } else { 27 | if (chat?.userId !== userId) { 28 | notFound() 29 | } 30 | 31 | return ( 32 | 33 | 39 | 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /agent-ui/app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ArtifactProvider } from '@/components/contexts/artifact-context' 2 | import { SidebarDesktop } from '@/components/sidebar-desktop' 3 | 4 | interface ChatLayoutProps { 5 | children: React.ReactNode 6 | } 7 | 8 | export default async function ChatLayout({ children }: ChatLayoutProps) { 9 | return ( 10 | 11 |
12 | 13 | {children} 14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /agent-ui/app/(chat)/page.tsx: -------------------------------------------------------------------------------- 1 | import { AI } from '@/app/(chat)/actions' 2 | import { auth } from '@clerk/nextjs/server' 3 | 4 | import { getMissingKeys } from '@/app/actions' 5 | import { ChatWithArtifacts } from '@/components/chat-with-artifacts' 6 | import { generateId } from 'ai' 7 | 8 | export default async function IndexPage() { 9 | const id = generateId() 10 | const { userId } = auth() 11 | const missingKeys = await getMissingKeys() 12 | 13 | return ( 14 | 15 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /agent-ui/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /agent-ui/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GeistMono } from 'geist/font/mono' 2 | import { GeistSans } from 'geist/font/sans' 3 | 4 | import '@/app/globals.css' 5 | import { Providers } from '@/components/providers' 6 | import { TailwindIndicator } from '@/components/tailwind-indicator' 7 | import { Toaster } from '@/components/ui/sonner' 8 | import { cn } from '@/lib/utils' 9 | import { NextFontWithVariable } from 'next/dist/compiled/@next/font' 10 | import { DM_Serif_Display, Poppins, Urbanist } from 'next/font/google' 11 | 12 | const fontUrban: NextFontWithVariable = Urbanist({ 13 | subsets: ['latin'], 14 | variable: '--font-urban' 15 | }) 16 | 17 | const fontPoppins: NextFontWithVariable = Poppins({ 18 | subsets: ['latin'], 19 | variable: '--font-poppins', 20 | weight: ['400', '500', '600', '700'] 21 | }) 22 | 23 | const fontDmSansDisplay: NextFontWithVariable = DM_Serif_Display({ 24 | subsets: ['latin'], 25 | variable: '--font-dmSansDisplay', 26 | weight: ['400'] 27 | }) 28 | 29 | export const metadata = { 30 | metadataBase: process.env.VERCEL_URL 31 | ? new URL(`https://${process.env.VERCEL_URL}`) 32 | : undefined, 33 | title: { 34 | default: 'PostBot 3000 AI Agent', 35 | template: `%s - PostBot 3000 AI Agent` 36 | }, 37 | description: 38 | 'PostBot 3000 is your AI Social Media Assistant, designed to generate, schedule, and post content seamlessly across X (formerly Twitter) and LinkedIn. Enhance your social media strategy with intelligent automation.', 39 | icons: { 40 | icon: '/favicon.ico' 41 | } 42 | } 43 | 44 | interface RootLayoutProps { 45 | children: React.ReactNode 46 | } 47 | 48 | export default function RootLayout({ children }: RootLayoutProps) { 49 | return ( 50 | 51 | 61 | 66 | 71 |
72 |
{children}
73 |
74 | 75 |
76 | 77 | 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /agent-ui/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmad2b/postbot3000/a77e5dcf19a107fa49e38e32ee77e1490e3d82cf/agent-ui/app/opengraph-image.png -------------------------------------------------------------------------------- /agent-ui/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmad2b/postbot3000/a77e5dcf19a107fa49e38e32ee77e1490e3d82cf/agent-ui/app/twitter-image.png -------------------------------------------------------------------------------- /agent-ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /agent-ui/components/button-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { Button, type ButtonProps } from '@/components/ui/button' 7 | import { IconArrowDown } from '@/components/ui/icons' 8 | 9 | interface ButtonScrollToBottomProps extends ButtonProps { 10 | isAtBottom: boolean 11 | scrollToBottom: () => void 12 | } 13 | 14 | export function ButtonScrollToBottom({ 15 | className, 16 | isAtBottom, 17 | scrollToBottom, 18 | ...props 19 | }: ButtonScrollToBottomProps) { 20 | return ( 21 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /agent-ui/components/chat-history.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { SidebarList } from '@/components/sidebar-list' 4 | 5 | interface ChatHistoryProps { 6 | userId?: string 7 | } 8 | 9 | export async function ChatHistory({ userId }: ChatHistoryProps) { 10 | return ( 11 |
12 |
13 |

Post History

14 |
15 | {/*
16 | 23 | 24 | New Chat 25 | 26 |
*/} 27 | 30 | {Array.from({ length: 10 }).map((_, i) => ( 31 |
35 | ))} 36 |
37 | } 38 | > 39 | {/* @ts-ignore */} 40 | 41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /agent-ui/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { UIState } from '@/app/(chat)/actions' 2 | import { Separator } from '@/components/ui/separator' 3 | import { cn } from '@/lib/utils' 4 | import { useSearchParams } from 'next/navigation' 5 | import React, { useEffect } from 'react' 6 | import { useArtifacts } from './contexts/artifact-context' 7 | 8 | export interface ChatList { 9 | messages: UIState[] 10 | userId: string | null 11 | isShared: boolean 12 | onArtifactClick?: (artifact: { 13 | id: string 14 | type: string 15 | content: React.ReactNode 16 | }) => void 17 | } 18 | 19 | export function ChatList({ 20 | messages, 21 | userId, 22 | isShared, 23 | onArtifactClick 24 | }: ChatList) { 25 | const { artifacts, addArtifact, selectedArtifact, setSelectedArtifact } = 26 | useArtifacts() 27 | 28 | if (!messages.length) { 29 | return null 30 | } 31 | 32 | const searchParams = useSearchParams() 33 | const artifactId = searchParams.get('artifactId') 34 | 35 | useEffect(() => { 36 | console.log('artifactId', artifactId) 37 | console.log('artifacts', artifacts) 38 | 39 | if (artifactId && typeof artifactId === 'string') { 40 | const artifact = artifacts.find(a => a.id === artifactId) 41 | if (artifact) { 42 | setSelectedArtifact(artifact) 43 | } 44 | } 45 | }, [artifacts, setSelectedArtifact]) 46 | 47 | return ( 48 |
49 | {messages.map((message, index) => ( 50 |
51 | {React.cloneElement(message.display as React.ReactElement, { 52 | onArtifactClick 53 | })} 54 | {index < messages.length - 1 && } 55 |
56 | ))} 57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /agent-ui/components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Message } from '@/lib/types' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { IconCheck, IconCopy } from '@/components/ui/icons' 7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 8 | import { cn } from '@/lib/utils' 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 11 | message: Message 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 20 | 21 | const onCopy = () => { 22 | if (isCopied) return 23 | copyToClipboard(message.content) 24 | } 25 | 26 | return ( 27 |
34 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /agent-ui/components/chat-message.tsx: -------------------------------------------------------------------------------- 1 | import remarkGfm from 'remark-gfm' 2 | import remarkMath from 'remark-math' 3 | 4 | import { ChatMessageActions } from '@/components/chat-message-actions' 5 | import { MemoizedReactMarkdown } from '@/components/markdown' 6 | import { CodeBlock } from '@/components/ui/codeblock' 7 | import { IconOpenAI, IconUser } from '@/components/ui/icons' 8 | import { Message } from '@/lib/types' 9 | import { cn } from '@/lib/utils' 10 | 11 | export interface ChatMessageProps { 12 | message: Message 13 | } 14 | 15 | export function ChatMessage({ message, ...props }: ChatMessageProps) { 16 | return ( 17 |
21 |
29 | {message.role === 'human' ? : } 30 |
31 |
32 | {children}

38 | }, 39 | code({ node, inline, className, children, ...props }) { 40 | if (children.length) { 41 | if (children[0] == '▍') { 42 | return ( 43 | 44 | ) 45 | } 46 | 47 | children[0] = (children[0] as string).replace('`▍`', '▍') 48 | } 49 | 50 | const match = /language-(\w+)/.exec(className || '') 51 | 52 | if (inline) { 53 | return ( 54 | 55 | {children} 56 | 57 | ) 58 | } 59 | 60 | return ( 61 | 67 | ) 68 | } 69 | }} 70 | > 71 | {message.content} 72 |
73 | 74 |
75 |
76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /agent-ui/components/chat-panel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import type { AI } from '@/app/(chat)/actions' 4 | import { shareChat } from '@/app/actions' 5 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' 6 | import { ChatShareDialog } from '@/components/chat-share-dialog' 7 | import { useArtifacts } from '@/components/contexts/artifact-context' 8 | import { FooterText } from '@/components/footer' 9 | import { PromptForm } from '@/components/prompt-form' 10 | import { Button } from '@/components/ui/button' 11 | import { IconShare } from '@/components/ui/icons' 12 | import { cn } from '@/lib/utils' 13 | import { useAIState, useUIState } from 'ai/rsc' 14 | 15 | export interface ChatPanelProps { 16 | chatId?: string 17 | title?: string 18 | input: string 19 | setInput: (value: string) => void 20 | isAtBottom: boolean 21 | scrollToBottom: () => void 22 | onArtifactCreated?: (artifact: { 23 | id: string 24 | type: string 25 | content: React.ReactNode 26 | }) => void 27 | } 28 | 29 | export function ChatPanel({ 30 | chatId, 31 | title, 32 | input, 33 | setInput, 34 | isAtBottom, 35 | scrollToBottom, 36 | onArtifactCreated 37 | }: ChatPanelProps) { 38 | const [aiState] = useAIState() 39 | const [messages, setMessages] = useUIState() 40 | const [shareDialogOpen, setShareDialogOpen] = React.useState(false) 41 | const { selectedArtifact } = useArtifacts() 42 | 43 | return ( 44 |
50 | 54 | 55 |
56 | {messages?.length >= 2 ? ( 57 |
58 |
59 | {chatId && title ? ( 60 | <> 61 | 68 | setShareDialogOpen(false)} 72 | shareChat={shareChat} 73 | chat={{ 74 | chatId, 75 | title, 76 | messages: aiState.messages 77 | }} 78 | /> 79 | 80 | ) : null} 81 |
82 |
83 | ) : null} 84 | 85 |
86 |
87 |
88 | 89 |
90 |
91 | 92 |
93 |
94 |
95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /agent-ui/components/chat-share-dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type DialogProps } from '@radix-ui/react-dialog' 4 | import * as React from 'react' 5 | import { toast } from 'sonner' 6 | 7 | import { Button } from '@/components/ui/button' 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogFooter, 13 | DialogHeader, 14 | DialogTitle 15 | } from '@/components/ui/dialog' 16 | import { IconSpinner } from '@/components/ui/icons' 17 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 18 | import { ServerActionResult, type Chat } from '@/lib/types' 19 | 20 | interface ChatShareDialogProps extends DialogProps { 21 | chat: Pick 22 | shareChat: (id: string) => ServerActionResult 23 | onCopy: () => void 24 | } 25 | 26 | export function ChatShareDialog({ 27 | chat, 28 | shareChat, 29 | onCopy, 30 | ...props 31 | }: ChatShareDialogProps) { 32 | const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 }) 33 | const [isSharePending, startShareTransition] = React.useTransition() 34 | 35 | const copyShareLink = React.useCallback( 36 | async (chat: Chat) => { 37 | if (!chat.sharePath) { 38 | return toast.error('Could not copy share link to clipboard') 39 | } 40 | 41 | const url = new URL(window.location.href) 42 | url.pathname = chat.sharePath 43 | copyToClipboard(url.toString()) 44 | onCopy() 45 | toast.success('Share link copied to clipboard') 46 | }, 47 | [copyToClipboard, onCopy] 48 | ) 49 | 50 | return ( 51 | 52 | 53 | 54 | Share link to chat 55 | 56 | Anyone with the URL will be able to view the shared chat. 57 | 58 | 59 |
60 |
{chat.title}
61 |
62 | {chat.messages.length} messages 63 |
64 |
65 | 66 | 91 | 92 |
93 |
94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /agent-ui/components/chat-with-artifacts.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Chat } from '@/components/chat' 4 | import { Button } from '@/components/ui/button' 5 | import { Message } from '@/lib/types' 6 | import { X } from 'lucide-react' 7 | import { useCallback } from 'react' 8 | import { useArtifacts } from './contexts/artifact-context' 9 | interface ChatWithArtifactsProps { 10 | chatId: string 11 | userId: string | null 12 | missingKeys: string[] 13 | initialMessages?: Message[] 14 | } 15 | 16 | export function ChatWithArtifacts({ 17 | chatId, 18 | userId, 19 | missingKeys, 20 | initialMessages 21 | }: ChatWithArtifactsProps) { 22 | const { addArtifact, selectedArtifact, setSelectedArtifact } = useArtifacts() 23 | 24 | const closeArtifact = useCallback(() => { 25 | setSelectedArtifact(null) 26 | }, [setSelectedArtifact]) 27 | 28 | return ( 29 |
30 |
33 | 41 |
42 | {selectedArtifact && ( 43 |
44 |
45 |

{selectedArtifact.type}

46 | 54 |
55 |
56 |
{selectedArtifact.content}
57 |
58 |
59 | )} 60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /agent-ui/components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { AI } from '@/app/(chat)/actions' 4 | import { ChatList } from '@/components/chat-list' 5 | import { ChatPanel } from '@/components/chat-panel' 6 | import { EmptyScreen } from '@/components/empty-screen' 7 | import { useLocalStorage } from '@/lib/hooks/use-local-storage' 8 | import { useScrollAnchor } from '@/lib/hooks/use-scroll-anchor' 9 | import { Message } from '@/lib/types' 10 | import { cn } from '@/lib/utils' 11 | import { useAIState, useUIState } from 'ai/rsc' 12 | import { useRouter } from 'next/navigation' 13 | import { useEffect, useState } from 'react' 14 | import { toast } from 'sonner' 15 | 16 | export interface ChatProps extends React.ComponentProps<'div'> { 17 | initialMessages?: Message[] 18 | chatId?: string 19 | userId: string | null 20 | missingKeys: string[] 21 | onArtifactCreated?: (artifact: { 22 | id: string 23 | type: string 24 | content: React.ReactNode 25 | }) => void 26 | onArtifactClicked?: (artifact: { 27 | id: string 28 | type: string 29 | content: React.ReactNode 30 | }) => void 31 | } 32 | 33 | export function Chat({ 34 | chatId, 35 | className, 36 | userId, 37 | missingKeys, 38 | onArtifactCreated, 39 | onArtifactClicked 40 | }: ChatProps) { 41 | const router = useRouter() 42 | const [input, setInput] = useState('') 43 | const [messages, setMessages] = useUIState() 44 | const [aiState] = useAIState() 45 | 46 | const [_, setNewChatId] = useLocalStorage('newChatId', chatId) 47 | 48 | useEffect(() => { 49 | const messagesLength = aiState.messages?.length 50 | if (messagesLength === 2) { 51 | router.refresh() 52 | } 53 | }, [aiState.messages, router]) 54 | 55 | useEffect(() => { 56 | setNewChatId(chatId) 57 | }, [chatId, setNewChatId]) 58 | 59 | useEffect(() => { 60 | missingKeys.map(key => { 61 | toast.error(`Missing ${key} environment variable!`) 62 | }) 63 | }, [missingKeys]) 64 | 65 | const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } = 66 | useScrollAnchor() 67 | 68 | if (messages.length === 0) { 69 | return ( 70 |
71 | 72 |
73 | ) 74 | } 75 | 76 | return ( 77 |
81 |
85 | {messages.length ? ( 86 | 92 | ) : ( 93 | 94 | )} 95 | {/*
*/} 96 |
97 | 98 | {messages.length === 0 ? null : ( 99 | 107 | )} 108 |
109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /agent-ui/components/clear-history.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useRouter } from 'next/navigation' 5 | import { toast } from 'sonner' 6 | 7 | import { ServerActionResult } from '@/lib/types' 8 | import { Button } from '@/components/ui/button' 9 | import { 10 | AlertDialog, 11 | AlertDialogAction, 12 | AlertDialogCancel, 13 | AlertDialogContent, 14 | AlertDialogDescription, 15 | AlertDialogFooter, 16 | AlertDialogHeader, 17 | AlertDialogTitle, 18 | AlertDialogTrigger 19 | } from '@/components/ui/alert-dialog' 20 | import { IconSpinner } from '@/components/ui/icons' 21 | 22 | interface ClearHistoryProps { 23 | isEnabled: boolean 24 | clearChats: () => ServerActionResult 25 | } 26 | 27 | export function ClearHistory({ 28 | isEnabled = false, 29 | clearChats 30 | }: ClearHistoryProps) { 31 | const [open, setOpen] = React.useState(false) 32 | const [isPending, startTransition] = React.useTransition() 33 | const router = useRouter() 34 | 35 | return ( 36 | 37 | 38 | 42 | 43 | 44 | 45 | Are you absolutely sure? 46 | 47 | This will permanently delete your chat history and remove your data 48 | from our servers. 49 | 50 | 51 | 52 | Cancel 53 | { 56 | event.preventDefault() 57 | startTransition(async () => { 58 | const result = await clearChats() 59 | if (result && 'error' in result) { 60 | toast.error(result.error) 61 | return 62 | } 63 | 64 | setOpen(false) 65 | }) 66 | }} 67 | > 68 | {isPending && } 69 | Delete 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /agent-ui/components/content-document.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { Artifact } from '@/lib/types' 5 | import { FileTextIcon } from '@radix-ui/react-icons' 6 | import { useArtifacts } from './contexts/artifact-context' 7 | 8 | interface ContentDocumentProps { 9 | id: string 10 | type: string 11 | name: string 12 | content: React.ReactNode 13 | } 14 | 15 | export const ContentDocument = ({ 16 | content, 17 | id, 18 | name, 19 | type 20 | }: ContentDocumentProps) => { 21 | const { addArtifact, setSelectedArtifact } = useArtifacts() 22 | 23 | const handleOpenArtifact = () => { 24 | const artifact: Artifact = { 25 | id, 26 | type, 27 | content 28 | } 29 | addArtifact(artifact) 30 | setSelectedArtifact(artifact) 31 | } 32 | 33 | return ( 34 |
38 | 41 |

{name}

42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /agent-ui/components/contexts/artifact-context.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { createContext, useCallback, useContext, useState } from 'react' 3 | 4 | interface Artifact { 5 | id: string 6 | type: string 7 | content: React.ReactNode 8 | } 9 | 10 | interface ArtifactContextType { 11 | artifacts: Artifact[] 12 | addArtifact: (artifact: Artifact) => void 13 | selectedArtifact: Artifact | null 14 | setSelectedArtifact: (artifact: Artifact | null) => void 15 | } 16 | 17 | const ArtifactContext = createContext( 18 | undefined 19 | ) 20 | 21 | export function ArtifactProvider({ children }: { children: React.ReactNode }) { 22 | const [artifacts, setArtifacts] = useState([]) 23 | const [selectedArtifact, setSelectedArtifact] = useState( 24 | null 25 | ) 26 | 27 | // const addArtifact = (artifact: Artifact) => { 28 | // setArtifacts(prevArtifacts => [...prevArtifacts, artifact]) 29 | // } 30 | 31 | const addArtifact = useCallback((artifact: Artifact) => { 32 | setArtifacts(prevArtifacts => { 33 | if (!prevArtifacts.some(a => a.id === artifact.id)) { 34 | return [...prevArtifacts, artifact] 35 | } 36 | return prevArtifacts 37 | }) 38 | }, []) 39 | 40 | const contextValue = { 41 | artifacts, 42 | addArtifact, 43 | selectedArtifact, 44 | setSelectedArtifact 45 | } 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ) 52 | } 53 | 54 | export function useArtifacts() { 55 | const context = useContext(ArtifactContext) 56 | if (context === undefined) { 57 | throw new Error('useArtifacts must be used within an ArtifactProvider') 58 | } 59 | return context 60 | } 61 | -------------------------------------------------------------------------------- /agent-ui/components/empty-screen.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { GeneratePostButton } from './generate-post' 3 | 4 | export function EmptyScreen() { 5 | return ( 6 |
7 |
8 |
9 | PostBot 3000 16 |
17 |

18 | Welcome to PostBot 3000! 19 |

20 |

21 | PostBot 3000 is your AI Social Media Agent, designed to generate, 22 | schedule, and post content seamlessly across X (formerly Twitter) and 23 | LinkedIn. 24 |

25 |
26 | 27 |
28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /agent-ui/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /agent-ui/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 6 | return ( 7 |

14 | PostBot 3000 may make mistakes. 15 | Please verify the content before posting. 16 |

17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /agent-ui/components/generate-post.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import type { AI } from '@/app/(chat)/actions' 3 | import { UserMessage } from '@/components/message' 4 | import { Button } from '@/components/ui/button' 5 | import { Dialog, DialogContent } from '@/components/ui/dialog' 6 | import { SignIn, useUser } from '@clerk/nextjs' 7 | import { generateId } from 'ai' 8 | import { useAIState, useActions, useUIState } from 'ai/rsc' 9 | import { useState } from 'react' 10 | 11 | export const GeneratePostButton = () => { 12 | const [aiState] = useAIState() 13 | const [messages, setMessages] = useUIState() 14 | const { showForm } = useActions() 15 | const { user } = useUser() 16 | const [showSignIn, setShowSignIn] = useState(false) 17 | 18 | const handleClicked = async () => { 19 | if (!user) { 20 | setShowSignIn(true) 21 | return 22 | // return 23 | } 24 | 25 | setMessages(currentMessages => [ 26 | ...currentMessages, 27 | { 28 | id: generateId(), 29 | role: 'human', 30 | display: ( 31 | 32 | PostBot 3000, generate engaging posts for me! 33 | 34 | ) 35 | } 36 | ]) 37 | 38 | const responseMessage = await showForm( 39 | 'PostBot 3000, generate engaging posts for me!' 40 | ) 41 | 42 | setMessages(currentMessages => [...currentMessages, responseMessage]) 43 | } 44 | 45 | const handleDialogClose = () => { 46 | setShowSignIn(false) 47 | } 48 | 49 | return ( 50 | <> 51 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /agent-ui/components/header.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import * as React from 'react' 4 | 5 | import { Button, buttonVariants } from '@/components/ui/button' 6 | import { UserMenu } from '@/components/user-menu' 7 | import { cn } from '@/lib/utils' 8 | import { auth, currentUser } from '@clerk/nextjs/server' 9 | import { ChatHistory } from './chat-history' 10 | import { SidebarMobile } from './sidebar-mobile' 11 | 12 | export async function UserOrLogin() { 13 | const { userId } = auth() 14 | 15 | const user = await currentUser() 16 | 17 | return ( 18 |
19 | {userId && user && ( 20 | <> 21 | 22 | 23 | 24 | {/* */} 25 | 26 | )} 27 |
28 | {userId && user ? ( 29 | 30 | ) : ( 31 | 34 | )} 35 |
36 |
37 | ) 38 | } 39 | 40 | export function Header() { 41 | return ( 42 |
43 |
44 |
45 | 46 | Logo 53 |

54 | PostBot 3000 55 |

56 | 57 |
58 | 59 |
60 | 61 |
    62 |
  • 63 | 71 | Home 72 | 73 |
  • 74 |
75 |
76 |
77 | 78 |
79 | }> 80 | 81 | 82 |
83 |
84 |
85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /agent-ui/components/loading-spinner-message.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2Icon } from 'lucide-react' 2 | 3 | export const LoadingSpinnerWithMessage = ({ message }: { message: string }) => ( 4 |
5 | 6 |
{message}
7 |
8 | ) 9 | -------------------------------------------------------------------------------- /agent-ui/components/login-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { signIn } from 'next-auth/react' 5 | 6 | import { cn } from '@/lib/utils' 7 | import { Button, type ButtonProps } from '@/components/ui/button' 8 | import { IconGitHub, IconSpinner } from '@/components/ui/icons' 9 | 10 | interface LoginButtonProps extends ButtonProps { 11 | showGithubIcon?: boolean 12 | text?: string 13 | } 14 | 15 | export function LoginButton({ 16 | text = 'Login with GitHub', 17 | showGithubIcon = true, 18 | className, 19 | ...props 20 | }: LoginButtonProps) { 21 | const [isLoading, setIsLoading] = React.useState(false) 22 | return ( 23 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /agent-ui/components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | const components = { 5 | h1: (props: any) => ( 6 |

7 | {props.children} 8 |

9 | ), 10 | h2: (props: any) => ( 11 |

12 | {props.children} 13 |

14 | ), 15 | h3: (props: any) => ( 16 |

17 | {props.children} 18 |

19 | ), 20 | h4: (props: any) => ( 21 |

22 | {props.children} 23 |

24 | ), 25 | p: (props: any) => ( 26 |

27 | {props.children} 28 |

29 | ), 30 | blockqoute: (props: any) => ( 31 |
32 | {props.children} 33 |
34 | ), 35 | ol: (props: any) => ( 36 |
    37 | {props.children} 38 |
39 | ), 40 | ul: (props: any) => ( 41 |
    42 | {props.children} 43 |
44 | ), 45 | sup: (props: any) => ( 46 | 47 | {props.children} 48 | 49 | ), 50 | a: ({ href, children }: any) => { 51 | const isInternalLink = href.startsWith('#') 52 | 53 | return ( 54 | 61 | {children} 62 | 63 | ) 64 | }, 65 | li: (props: any) => ( 66 |
  • 67 | {props.children} 68 |
  • 69 | ), 70 | footer: (props: any) => ( 71 |
    72 | {props.children} 73 |
    74 | ) 75 | } 76 | 77 | export const MemoizedReactMarkdown: FC = memo( 78 | props => , 79 | (prevProps, nextProps) => 80 | prevProps.children === nextProps.children && 81 | prevProps.className === nextProps.className 82 | ) 83 | -------------------------------------------------------------------------------- /agent-ui/components/message.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { MemoizedReactMarkdown } from '@/components/markdown' 4 | import { spinner } from '@/components/spinner' 5 | import { CodeBlock } from '@/components/ui/codeblock' 6 | import { useStreamableText } from '@/lib/hooks/use-streamable-text' 7 | import { cn } from '@/lib/utils' 8 | import { StreamableValue } from 'ai/rsc' 9 | import Image from 'next/image' 10 | import remarkGfm from 'remark-gfm' 11 | import remarkMath from 'remark-math' 12 | 13 | // Different types of message bubbles. 14 | 15 | export function UserMessage({ children }: { children: React.ReactNode }) { 16 | return ( 17 |
    18 |
    19 | User 20 |
    21 |
    22 |
    {children}
    23 |
    24 |
    25 | ) 26 | } 27 | 28 | export function BotMessage({ 29 | content, 30 | className 31 | }: { 32 | content: string | StreamableValue 33 | className?: string 34 | }) { 35 | const text = useStreamableText(content) 36 | 37 | return ( 38 |
    39 |
    40 | User 41 |
    42 |
    43 | {children}

    49 | }, 50 | code({ node, inline, className, children, ...props }) { 51 | if (children.length) { 52 | if (children[0] == '▍') { 53 | return ( 54 | 55 | ) 56 | } 57 | 58 | children[0] = (children[0] as string).replace('`▍`', '▍') 59 | } 60 | 61 | const match = /language-(\w+)/.exec(className || '') 62 | 63 | if (inline) { 64 | return ( 65 | 66 | {children} 67 | 68 | ) 69 | } 70 | 71 | return ( 72 | 78 | ) 79 | } 80 | }} 81 | > 82 | {text} 83 |
    84 |
    85 |
    86 | ) 87 | } 88 | 89 | export function BotCard({ 90 | children, 91 | showAvatar = true 92 | }: { 93 | children: React.ReactNode 94 | showAvatar?: boolean 95 | }) { 96 | return ( 97 |
    98 |
    104 | AI 105 |
    106 |
    107 |
    {children}
    108 |
    109 |
    110 | ) 111 | } 112 | 113 | export function SystemMessage({ children }: { children: React.ReactNode }) { 114 | return ( 115 |
    120 |
    {children}
    121 |
    122 | ) 123 | } 124 | 125 | export function SpinnerMessage() { 126 | return ( 127 |
    128 |
    129 | AI 130 |
    131 |
    132 | {spinner} 133 |
    134 |
    135 | ) 136 | } 137 | -------------------------------------------------------------------------------- /agent-ui/components/post-draft.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Image from 'next/image' 3 | import { toast } from 'sonner' 4 | import { Button } from './ui/button' 5 | 6 | interface PostDraftProps { 7 | platform: 'Twitter' | 'LinkedIn' 8 | } 9 | 10 | export const PostDraft = ({ platform }: PostDraftProps) => { 11 | const handleClick = () => { 12 | toast.info('This feature is on the way! 🚀') 13 | } 14 | 15 | return ( 16 |
    17 | 42 |
    43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /agent-ui/components/post-generator-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { AI } from '@/app/(chat)/actions' 4 | import { UserMessage } from '@/components/message' 5 | import { Button } from '@/components/ui/button' 6 | import { Input } from '@/components/ui/input' 7 | import { Label } from '@/components/ui/label' 8 | import { 9 | Select, 10 | SelectContent, 11 | SelectItem, 12 | SelectTrigger, 13 | SelectValue 14 | } from '@/components/ui/select' 15 | import { Textarea } from '@/components/ui/textarea' 16 | import { generateId } from 'ai' 17 | import { useAIState, useActions, useUIState } from 'ai/rsc' 18 | import { useState } from 'react' 19 | 20 | interface PostGeneratorFormProps { 21 | id: string 22 | initialPostDetails?: string 23 | initialAudience?: string 24 | initialDrafts?: string 25 | } 26 | 27 | export function PostGeneratorForm({ 28 | initialDrafts, 29 | initialAudience, 30 | initialPostDetails, 31 | id 32 | }: PostGeneratorFormProps) { 33 | const [aiState] = useAIState() 34 | const [messages, setMessages] = useUIState() 35 | const { sendMessage } = useActions() 36 | 37 | const [postDetails, setPostDetails] = useState(initialPostDetails || '') 38 | const [audience, setAudience] = useState(initialAudience || '') 39 | const [drafts, setDrafts] = useState(initialDrafts || '1') 40 | 41 | const handleSubmit = async (e: React.FormEvent) => { 42 | e.preventDefault() 43 | console.log('Form submitted', { postDetails, audience, drafts }) 44 | 45 | setMessages(currentMessages => [ 46 | ...currentMessages, 47 | { 48 | id: generateId(), 49 | role: 'human', 50 | display: ( 51 | 52 |
    53 |
    54 |

    Post Details:

    55 |

    {postDetails}

    56 |
    57 |
    58 |

    Audience:

    59 |

    {audience}

    60 |
    61 |
    62 |
    63 | ) 64 | } 65 | ]) 66 | 67 | const responseMessage = await sendMessage( 68 | postDetails, 69 | audience, 70 | parseInt(drafts), 71 | id 72 | ) 73 | 74 | setMessages(currentMessages => [...currentMessages, responseMessage]) 75 | } 76 | 77 | // const isButtonDisabled = 78 | // !!initialPostDetails?.length || !!initialAudience 79 | 80 | const isButtonDisabled: boolean = 81 | Boolean(initialPostDetails && initialPostDetails.length > 0) || 82 | Boolean(initialAudience && initialAudience.length > 0) 83 | 84 | return ( 85 |
    86 |
    87 |
    88 | 91 |