├── LICENSE ├── README.md ├── backend ├── .gitignore ├── app.py ├── hitlworkflow.py └── pyproject.toml ├── diagrams.graffle ├── flow.gif ├── flow.png └── frontend ├── .gitignore ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── src └── app │ ├── favicon.ico │ ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── page.module.css │ └── page.tsx └── tsconfig.json /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Laurie Voss 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Human in the Loop Workflow demo 2 | 3 | [Want to walk through this code? Check out this video.](https://www.youtube.com/watch?v=hf3_fuVdrpA) 4 | 5 | ![Human in the Loop Workflow](flow.gif) 6 | 7 | This is a full-stack demonstration of how to build a LlamaIndex workflow that uses a human in the loop. 8 | 9 | The workflow is a dummy flow that simulates a report-writing agent: you give it a topic, it does research for you, and then presents that research. If you think the research is sufficient, you can approve it and the LLM will write a report based on the research. If you think the research is insufficient, you can reject it and the LLM will do more research. 10 | 11 | ![Workflow](flow.png) 12 | 13 | # Frontend 14 | 15 | This is a starter Next.js app. Its primary function is to open a websocket connection to the backend, and print out the events that come back. It handles 3 types of events: 16 | 17 | 1. `progress` events, which the workflow emits to report on its progress. It simply prints these out. 18 | 19 | 2. `input_required` events, which the workflow emits when it needs human input. It prints out the payload, and generates two buttons that the human can use to accept or reject the research. 20 | 21 | 3. `final_result` events, which the workflow emits when it is complete. It prints out the final result. 22 | 23 | All the logic is handled by the backend, and the frontend is just a display. 24 | 25 | # Backend 26 | 27 | This is a FastAPI app. It has a websocket endpoint that the frontend connects to. It uses the `HITLWorkflow` class to run the workflow. 28 | 29 | ## FastAPI 30 | 31 | This does a few simple things: 32 | 33 | 1. Accepts a websocket connection from the frontend 34 | 2. Instantiates the workflow with the first message it gets from the frontend as the query. 35 | 3. Streams events from the workflow back to the frontend; these are the three types of events described above. 36 | * If it gets an `InputRequiredEvent`, it expects the next message from the frontend to be the approval or rejection of the research. 37 | * Once it gets the approval or rejection, it sends that back to the workflow as a `HumanResponseEvent`. 38 | * If it gets a `ProgressEvent`, it sends that back to the frontend. 39 | 4. Once the workflow is complete, it sends the final result back to the frontend. 40 | 5. Handles websocket cleanup once the workflow is complete. 41 | 42 | ## Workflow 43 | 44 | Both the frontend and the fastAPI are primarily message-passing, and the real work happens (or would happen, if it weren't a dummy) in the workflow itself, which follows the pattern in the diagram at the top of this page. 45 | 46 | -------------------------------------------------------------------------------- /backend/.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 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /backend/app.py: -------------------------------------------------------------------------------- 1 | # bring in our imports 2 | from fastapi import FastAPI, WebSocket 3 | from pydantic import BaseModel 4 | from hitlworkflow import HITLWorkflow, ProgressEvent 5 | from llama_index.core.workflow.events import ( 6 | InputRequiredEvent, 7 | HumanResponseEvent 8 | ) 9 | from llama_index.core.workflow.handler import WorkflowHandler 10 | 11 | # create our FastAPI app 12 | app = FastAPI() 13 | 14 | # create a websocket endpoint for our app 15 | @app.websocket("/query") 16 | async def query_endpoint(websocket: WebSocket): 17 | await websocket.accept() 18 | 19 | # instantiate our workflow with no timeout 20 | workflow = HITLWorkflow(timeout=None, verbose=False) 21 | 22 | try: 23 | # the first thing we should receive is a query 24 | query_data = await websocket.receive_json() 25 | # we pass it to the workflow 26 | handler: WorkflowHandler = workflow.run(query=query_data["question"]) 27 | 28 | # now we handle events coming back from the workflow 29 | async for event in handler.stream_events(): 30 | # if we get an InputRequiredEvent, that means the workflow needs human input 31 | # so we send an event to the frontend that will be handled specially 32 | if isinstance(event, InputRequiredEvent): 33 | await websocket.send_json({ 34 | "type": "input_required", 35 | "payload": event.payload 36 | }) 37 | # we expect the next thing from the socket to be human input 38 | response = await websocket.receive_json() 39 | # which we send back to the workflow as a HumanResponseEvent 40 | handler.ctx.send_event(HumanResponseEvent(response=response["response"])) 41 | elif isinstance(event, ProgressEvent): 42 | # the workflow also emits progress events which we send to the frontend 43 | await websocket.send_json({ 44 | "type": "progress", 45 | "payload": str(event.msg) 46 | }) 47 | 48 | # this only happens when the workflow is complete 49 | final_result = await handler 50 | await websocket.send_json({ 51 | "type": "final_result", 52 | "payload": str(final_result) 53 | }) 54 | 55 | except Exception as e: 56 | await websocket.send_json({"type": "error", "payload": str(e)}) 57 | finally: 58 | await websocket.close() 59 | 60 | if __name__ == "__main__": 61 | import uvicorn 62 | uvicorn.run(app, host="0.0.0.0", port=8000) 63 | -------------------------------------------------------------------------------- /backend/hitlworkflow.py: -------------------------------------------------------------------------------- 1 | from llama_index.core.workflow import ( 2 | Workflow, 3 | step, 4 | Event, 5 | Context 6 | ) 7 | from llama_index.core.workflow.events import ( 8 | StartEvent, 9 | StopEvent, 10 | InputRequiredEvent, 11 | HumanResponseEvent 12 | ) 13 | 14 | # some event types to define the workflow: 15 | 16 | # if the user says the research is not good enough, we retry the workflow 17 | class RetryEvent(Event): 18 | pass 19 | 20 | # if the user says the research is good enough, we generate a report 21 | class ReportEvent(Event): 22 | pass 23 | 24 | # we emit progress events to the frontend so the user knows what's happening 25 | class ProgressEvent(Event): 26 | pass 27 | 28 | # this is a dummy workflow to show how to do human in the loop workflows 29 | # the purpose of the flow is to research a topic, get human review, and then write a report 30 | class HITLWorkflow(Workflow): 31 | 32 | # this does the "research", which might involve searching the web or 33 | # looking up data in a database or our vector store. 34 | @step 35 | async def research_query(self, ctx: Context, ev: StartEvent | RetryEvent) -> InputRequiredEvent: 36 | ctx.write_event_to_stream(ProgressEvent(msg=f"I am doing some research on the subject of '{ev.query}'")) 37 | await ctx.set("original_query", ev.query) 38 | 39 | # once we've done the research, we send what we've found back to the human for review 40 | # this gets handled by the frontend, and we expect a HumanResponseEvent to be sent back 41 | return InputRequiredEvent(prefix="", query=ev.query,payload=f"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut purus eget sapien. Nulla facilisi. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") 42 | 43 | # this accepts the HumanResponseEvent, which is either approval or rejection 44 | # if it's approval, we write the report, otherwise we do more research 45 | @step 46 | async def human_review(self, ctx: Context, ev: HumanResponseEvent) -> ReportEvent | RetryEvent: 47 | ctx.write_event_to_stream(ProgressEvent(msg=f"The human has responded: {ev.response}")) 48 | if (ev.response == "yes"): 49 | return ReportEvent(result=f"Here is the research on {await ctx.get('original_query')}") 50 | else: 51 | ctx.write_event_to_stream(ProgressEvent(msg=f"The human has rejected the research, retrying")) 52 | return RetryEvent(query=await ctx.get("original_query")) 53 | 54 | # this write the report, which would be an LLM operation with a bunch of context. 55 | @step 56 | async def write_report(self, ctx: Context, ev: ReportEvent) -> StopEvent: 57 | ctx.write_event_to_stream(ProgressEvent(msg=f"The human has approved the research, generating final report")) 58 | # generate a report here 59 | return StopEvent(result=f"This is a report on {await ctx.get('original_query')}") 60 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "backend" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Laurie Voss "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | 11 | 12 | [build-system] 13 | requires = ["poetry-core"] 14 | build-backend = "poetry.core.masonry.api" 15 | -------------------------------------------------------------------------------- /diagrams.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/run-llama/human_in_the_loop_workflow_demo/abdf4dd874a4050e3f5c67a243bd5b529cf14288/diagrams.graffle -------------------------------------------------------------------------------- /flow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/run-llama/human_in_the_loop_workflow_demo/abdf4dd874a4050e3f5c67a243bd5b529cf14288/flow.gif -------------------------------------------------------------------------------- /flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/run-llama/human_in_the_loop_workflow_demo/abdf4dd874a4050e3f5c67a243bd5b529cf14288/flow.png -------------------------------------------------------------------------------- /frontend/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /frontend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hitl", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hitl", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "next": "14.2.13", 12 | "react": "^18", 13 | "react-dom": "^18" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^20", 17 | "@types/react": "^18", 18 | "@types/react-dom": "^18", 19 | "typescript": "^5" 20 | } 21 | }, 22 | "node_modules/@next/env": { 23 | "version": "14.2.13", 24 | "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.13.tgz", 25 | "integrity": "sha512-s3lh6K8cbW1h5Nga7NNeXrbe0+2jIIYK9YaA9T7IufDWnZpozdFUp6Hf0d5rNWUKu4fEuSX2rCKlGjCrtylfDw==", 26 | "license": "MIT" 27 | }, 28 | "node_modules/@next/swc-darwin-arm64": { 29 | "version": "14.2.13", 30 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.13.tgz", 31 | "integrity": "sha512-IkAmQEa2Htq+wHACBxOsslt+jMoV3msvxCn0WFSfJSkv/scy+i/EukBKNad36grRxywaXUYJc9mxEGkeIs8Bzg==", 32 | "cpu": [ 33 | "arm64" 34 | ], 35 | "license": "MIT", 36 | "optional": true, 37 | "os": [ 38 | "darwin" 39 | ], 40 | "engines": { 41 | "node": ">= 10" 42 | } 43 | }, 44 | "node_modules/@next/swc-darwin-x64": { 45 | "version": "14.2.13", 46 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.13.tgz", 47 | "integrity": "sha512-Dv1RBGs2TTjkwEnFMVL5XIfJEavnLqqwYSD6LXgTPdEy/u6FlSrLBSSfe1pcfqhFEXRAgVL3Wpjibe5wXJzWog==", 48 | "cpu": [ 49 | "x64" 50 | ], 51 | "license": "MIT", 52 | "optional": true, 53 | "os": [ 54 | "darwin" 55 | ], 56 | "engines": { 57 | "node": ">= 10" 58 | } 59 | }, 60 | "node_modules/@next/swc-linux-arm64-gnu": { 61 | "version": "14.2.13", 62 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.13.tgz", 63 | "integrity": "sha512-yB1tYEFFqo4ZNWkwrJultbsw7NPAAxlPXURXioRl9SdW6aIefOLS+0TEsKrWBtbJ9moTDgU3HRILL6QBQnMevg==", 64 | "cpu": [ 65 | "arm64" 66 | ], 67 | "license": "MIT", 68 | "optional": true, 69 | "os": [ 70 | "linux" 71 | ], 72 | "engines": { 73 | "node": ">= 10" 74 | } 75 | }, 76 | "node_modules/@next/swc-linux-arm64-musl": { 77 | "version": "14.2.13", 78 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.13.tgz", 79 | "integrity": "sha512-v5jZ/FV/eHGoWhMKYrsAweQ7CWb8xsWGM/8m1mwwZQ/sutJjoFaXchwK4pX8NqwImILEvQmZWyb8pPTcP7htWg==", 80 | "cpu": [ 81 | "arm64" 82 | ], 83 | "license": "MIT", 84 | "optional": true, 85 | "os": [ 86 | "linux" 87 | ], 88 | "engines": { 89 | "node": ">= 10" 90 | } 91 | }, 92 | "node_modules/@next/swc-linux-x64-gnu": { 93 | "version": "14.2.13", 94 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.13.tgz", 95 | "integrity": "sha512-aVc7m4YL7ViiRv7SOXK3RplXzOEe/qQzRA5R2vpXboHABs3w8vtFslGTz+5tKiQzWUmTmBNVW0UQdhkKRORmGA==", 96 | "cpu": [ 97 | "x64" 98 | ], 99 | "license": "MIT", 100 | "optional": true, 101 | "os": [ 102 | "linux" 103 | ], 104 | "engines": { 105 | "node": ">= 10" 106 | } 107 | }, 108 | "node_modules/@next/swc-linux-x64-musl": { 109 | "version": "14.2.13", 110 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.13.tgz", 111 | "integrity": "sha512-4wWY7/OsSaJOOKvMsu1Teylku7vKyTuocvDLTZQq0TYv9OjiYYWt63PiE1nTuZnqQ4RPvME7Xai+9enoiN0Wrg==", 112 | "cpu": [ 113 | "x64" 114 | ], 115 | "license": "MIT", 116 | "optional": true, 117 | "os": [ 118 | "linux" 119 | ], 120 | "engines": { 121 | "node": ">= 10" 122 | } 123 | }, 124 | "node_modules/@next/swc-win32-arm64-msvc": { 125 | "version": "14.2.13", 126 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.13.tgz", 127 | "integrity": "sha512-uP1XkqCqV2NVH9+g2sC7qIw+w2tRbcMiXFEbMihkQ8B1+V6m28sshBwAB0SDmOe0u44ne1vFU66+gx/28RsBVQ==", 128 | "cpu": [ 129 | "arm64" 130 | ], 131 | "license": "MIT", 132 | "optional": true, 133 | "os": [ 134 | "win32" 135 | ], 136 | "engines": { 137 | "node": ">= 10" 138 | } 139 | }, 140 | "node_modules/@next/swc-win32-ia32-msvc": { 141 | "version": "14.2.13", 142 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.13.tgz", 143 | "integrity": "sha512-V26ezyjPqQpDBV4lcWIh8B/QICQ4v+M5Bo9ykLN+sqeKKBxJVDpEc6biDVyluTXTC40f5IqCU0ttth7Es2ZuMw==", 144 | "cpu": [ 145 | "ia32" 146 | ], 147 | "license": "MIT", 148 | "optional": true, 149 | "os": [ 150 | "win32" 151 | ], 152 | "engines": { 153 | "node": ">= 10" 154 | } 155 | }, 156 | "node_modules/@next/swc-win32-x64-msvc": { 157 | "version": "14.2.13", 158 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.13.tgz", 159 | "integrity": "sha512-WwzOEAFBGhlDHE5Z73mNU8CO8mqMNLqaG+AO9ETmzdCQlJhVtWZnOl2+rqgVQS+YHunjOWptdFmNfbpwcUuEsw==", 160 | "cpu": [ 161 | "x64" 162 | ], 163 | "license": "MIT", 164 | "optional": true, 165 | "os": [ 166 | "win32" 167 | ], 168 | "engines": { 169 | "node": ">= 10" 170 | } 171 | }, 172 | "node_modules/@swc/counter": { 173 | "version": "0.1.3", 174 | "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", 175 | "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", 176 | "license": "Apache-2.0" 177 | }, 178 | "node_modules/@swc/helpers": { 179 | "version": "0.5.5", 180 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", 181 | "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", 182 | "license": "Apache-2.0", 183 | "dependencies": { 184 | "@swc/counter": "^0.1.3", 185 | "tslib": "^2.4.0" 186 | } 187 | }, 188 | "node_modules/@types/node": { 189 | "version": "20.16.8", 190 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.8.tgz", 191 | "integrity": "sha512-sbo5JmfbZNkyDv+2HCccr9Y9ZkKJBMTru7UdAsCojMGjKNjdaOV73bqEW242QrHEZL8R4LbHMrW+FHB5lZ5/bw==", 192 | "dev": true, 193 | "license": "MIT", 194 | "dependencies": { 195 | "undici-types": "~6.19.2" 196 | } 197 | }, 198 | "node_modules/@types/prop-types": { 199 | "version": "15.7.13", 200 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", 201 | "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", 202 | "dev": true, 203 | "license": "MIT" 204 | }, 205 | "node_modules/@types/react": { 206 | "version": "18.3.9", 207 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz", 208 | "integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==", 209 | "dev": true, 210 | "license": "MIT", 211 | "dependencies": { 212 | "@types/prop-types": "*", 213 | "csstype": "^3.0.2" 214 | } 215 | }, 216 | "node_modules/@types/react-dom": { 217 | "version": "18.3.0", 218 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", 219 | "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", 220 | "dev": true, 221 | "license": "MIT", 222 | "dependencies": { 223 | "@types/react": "*" 224 | } 225 | }, 226 | "node_modules/busboy": { 227 | "version": "1.6.0", 228 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 229 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 230 | "dependencies": { 231 | "streamsearch": "^1.1.0" 232 | }, 233 | "engines": { 234 | "node": ">=10.16.0" 235 | } 236 | }, 237 | "node_modules/caniuse-lite": { 238 | "version": "1.0.30001663", 239 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", 240 | "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", 241 | "funding": [ 242 | { 243 | "type": "opencollective", 244 | "url": "https://opencollective.com/browserslist" 245 | }, 246 | { 247 | "type": "tidelift", 248 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 249 | }, 250 | { 251 | "type": "github", 252 | "url": "https://github.com/sponsors/ai" 253 | } 254 | ], 255 | "license": "CC-BY-4.0" 256 | }, 257 | "node_modules/client-only": { 258 | "version": "0.0.1", 259 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 260 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", 261 | "license": "MIT" 262 | }, 263 | "node_modules/csstype": { 264 | "version": "3.1.3", 265 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 266 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 267 | "dev": true, 268 | "license": "MIT" 269 | }, 270 | "node_modules/graceful-fs": { 271 | "version": "4.2.11", 272 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 273 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 274 | "license": "ISC" 275 | }, 276 | "node_modules/js-tokens": { 277 | "version": "4.0.0", 278 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 279 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 280 | "license": "MIT" 281 | }, 282 | "node_modules/loose-envify": { 283 | "version": "1.4.0", 284 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 285 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 286 | "license": "MIT", 287 | "dependencies": { 288 | "js-tokens": "^3.0.0 || ^4.0.0" 289 | }, 290 | "bin": { 291 | "loose-envify": "cli.js" 292 | } 293 | }, 294 | "node_modules/nanoid": { 295 | "version": "3.3.7", 296 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 297 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 298 | "funding": [ 299 | { 300 | "type": "github", 301 | "url": "https://github.com/sponsors/ai" 302 | } 303 | ], 304 | "license": "MIT", 305 | "bin": { 306 | "nanoid": "bin/nanoid.cjs" 307 | }, 308 | "engines": { 309 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 310 | } 311 | }, 312 | "node_modules/next": { 313 | "version": "14.2.13", 314 | "resolved": "https://registry.npmjs.org/next/-/next-14.2.13.tgz", 315 | "integrity": "sha512-BseY9YNw8QJSwLYD7hlZzl6QVDoSFHL/URN5K64kVEVpCsSOWeyjbIGK+dZUaRViHTaMQX8aqmnn0PHBbGZezg==", 316 | "license": "MIT", 317 | "dependencies": { 318 | "@next/env": "14.2.13", 319 | "@swc/helpers": "0.5.5", 320 | "busboy": "1.6.0", 321 | "caniuse-lite": "^1.0.30001579", 322 | "graceful-fs": "^4.2.11", 323 | "postcss": "8.4.31", 324 | "styled-jsx": "5.1.1" 325 | }, 326 | "bin": { 327 | "next": "dist/bin/next" 328 | }, 329 | "engines": { 330 | "node": ">=18.17.0" 331 | }, 332 | "optionalDependencies": { 333 | "@next/swc-darwin-arm64": "14.2.13", 334 | "@next/swc-darwin-x64": "14.2.13", 335 | "@next/swc-linux-arm64-gnu": "14.2.13", 336 | "@next/swc-linux-arm64-musl": "14.2.13", 337 | "@next/swc-linux-x64-gnu": "14.2.13", 338 | "@next/swc-linux-x64-musl": "14.2.13", 339 | "@next/swc-win32-arm64-msvc": "14.2.13", 340 | "@next/swc-win32-ia32-msvc": "14.2.13", 341 | "@next/swc-win32-x64-msvc": "14.2.13" 342 | }, 343 | "peerDependencies": { 344 | "@opentelemetry/api": "^1.1.0", 345 | "@playwright/test": "^1.41.2", 346 | "react": "^18.2.0", 347 | "react-dom": "^18.2.0", 348 | "sass": "^1.3.0" 349 | }, 350 | "peerDependenciesMeta": { 351 | "@opentelemetry/api": { 352 | "optional": true 353 | }, 354 | "@playwright/test": { 355 | "optional": true 356 | }, 357 | "sass": { 358 | "optional": true 359 | } 360 | } 361 | }, 362 | "node_modules/picocolors": { 363 | "version": "1.1.0", 364 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", 365 | "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", 366 | "license": "ISC" 367 | }, 368 | "node_modules/postcss": { 369 | "version": "8.4.31", 370 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 371 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 372 | "funding": [ 373 | { 374 | "type": "opencollective", 375 | "url": "https://opencollective.com/postcss/" 376 | }, 377 | { 378 | "type": "tidelift", 379 | "url": "https://tidelift.com/funding/github/npm/postcss" 380 | }, 381 | { 382 | "type": "github", 383 | "url": "https://github.com/sponsors/ai" 384 | } 385 | ], 386 | "license": "MIT", 387 | "dependencies": { 388 | "nanoid": "^3.3.6", 389 | "picocolors": "^1.0.0", 390 | "source-map-js": "^1.0.2" 391 | }, 392 | "engines": { 393 | "node": "^10 || ^12 || >=14" 394 | } 395 | }, 396 | "node_modules/react": { 397 | "version": "18.3.1", 398 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", 399 | "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", 400 | "license": "MIT", 401 | "dependencies": { 402 | "loose-envify": "^1.1.0" 403 | }, 404 | "engines": { 405 | "node": ">=0.10.0" 406 | } 407 | }, 408 | "node_modules/react-dom": { 409 | "version": "18.3.1", 410 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", 411 | "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", 412 | "license": "MIT", 413 | "dependencies": { 414 | "loose-envify": "^1.1.0", 415 | "scheduler": "^0.23.2" 416 | }, 417 | "peerDependencies": { 418 | "react": "^18.3.1" 419 | } 420 | }, 421 | "node_modules/scheduler": { 422 | "version": "0.23.2", 423 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", 424 | "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", 425 | "license": "MIT", 426 | "dependencies": { 427 | "loose-envify": "^1.1.0" 428 | } 429 | }, 430 | "node_modules/source-map-js": { 431 | "version": "1.2.1", 432 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 433 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 434 | "license": "BSD-3-Clause", 435 | "engines": { 436 | "node": ">=0.10.0" 437 | } 438 | }, 439 | "node_modules/streamsearch": { 440 | "version": "1.1.0", 441 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 442 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 443 | "engines": { 444 | "node": ">=10.0.0" 445 | } 446 | }, 447 | "node_modules/styled-jsx": { 448 | "version": "5.1.1", 449 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 450 | "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 451 | "license": "MIT", 452 | "dependencies": { 453 | "client-only": "0.0.1" 454 | }, 455 | "engines": { 456 | "node": ">= 12.0.0" 457 | }, 458 | "peerDependencies": { 459 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 460 | }, 461 | "peerDependenciesMeta": { 462 | "@babel/core": { 463 | "optional": true 464 | }, 465 | "babel-plugin-macros": { 466 | "optional": true 467 | } 468 | } 469 | }, 470 | "node_modules/tslib": { 471 | "version": "2.7.0", 472 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", 473 | "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", 474 | "license": "0BSD" 475 | }, 476 | "node_modules/typescript": { 477 | "version": "5.6.2", 478 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", 479 | "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", 480 | "dev": true, 481 | "license": "Apache-2.0", 482 | "bin": { 483 | "tsc": "bin/tsc", 484 | "tsserver": "bin/tsserver" 485 | }, 486 | "engines": { 487 | "node": ">=14.17" 488 | } 489 | }, 490 | "node_modules/undici-types": { 491 | "version": "6.19.8", 492 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 493 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 494 | "dev": true, 495 | "license": "MIT" 496 | } 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hitl", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "react": "^18", 13 | "react-dom": "^18", 14 | "next": "14.2.13" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5", 18 | "@types/node": "^20", 19 | "@types/react": "^18", 20 | "@types/react-dom": "^18" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/run-llama/human_in_the_loop_workflow_demo/abdf4dd874a4050e3f5c67a243bd5b529cf14288/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/run-llama/human_in_the_loop_workflow_demo/abdf4dd874a4050e3f5c67a243bd5b529cf14288/frontend/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /frontend/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/run-llama/human_in_the_loop_workflow_demo/abdf4dd874a4050e3f5c67a243bd5b529cf14288/frontend/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | font-family: Arial, Helvetica, sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | padding: 0; 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | html { 40 | color-scheme: dark; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Human in the Loop Demo", 18 | description: "Generated by create next app", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 29 | {children} 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | --gray-rgb: 0, 0, 0; 3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08); 4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05); 5 | 6 | --button-primary-hover: #383838; 7 | --button-secondary-hover: #f2f2f2; 8 | 9 | display: grid; 10 | grid-template-rows: 20px 1fr 20px; 11 | align-items: center; 12 | justify-items: center; 13 | min-height: 100svh; 14 | padding: 80px; 15 | gap: 64px; 16 | font-family: var(--font-geist-sans); 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | .page { 21 | --gray-rgb: 255, 255, 255; 22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145); 23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06); 24 | 25 | --button-primary-hover: #ccc; 26 | --button-secondary-hover: #1a1a1a; 27 | } 28 | } 29 | 30 | .main { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 32px; 34 | grid-row-start: 2; 35 | } 36 | 37 | .main ol { 38 | font-family: var(--font-geist-mono); 39 | padding-left: 0; 40 | margin: 0; 41 | font-size: 14px; 42 | line-height: 24px; 43 | letter-spacing: -0.01em; 44 | list-style-position: inside; 45 | } 46 | 47 | .main li:not(:last-of-type) { 48 | margin-bottom: 8px; 49 | } 50 | 51 | .main code { 52 | font-family: inherit; 53 | background: var(--gray-alpha-100); 54 | padding: 2px 4px; 55 | border-radius: 4px; 56 | font-weight: 600; 57 | } 58 | 59 | .ctas { 60 | display: flex; 61 | gap: 16px; 62 | } 63 | 64 | .ctas a { 65 | appearance: none; 66 | border-radius: 128px; 67 | height: 48px; 68 | padding: 0 20px; 69 | border: none; 70 | border: 1px solid transparent; 71 | transition: background 0.2s, color 0.2s, border-color 0.2s; 72 | cursor: pointer; 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | font-size: 16px; 77 | line-height: 20px; 78 | font-weight: 500; 79 | } 80 | 81 | a.primary { 82 | background: var(--foreground); 83 | color: var(--background); 84 | gap: 8px; 85 | } 86 | 87 | a.secondary { 88 | border-color: var(--gray-alpha-200); 89 | min-width: 180px; 90 | } 91 | 92 | .footer { 93 | grid-row-start: 3; 94 | display: flex; 95 | gap: 24px; 96 | } 97 | 98 | .footer a { 99 | display: flex; 100 | align-items: center; 101 | gap: 8px; 102 | } 103 | 104 | .footer img { 105 | flex-shrink: 0; 106 | } 107 | 108 | /* Enable hover only on non-touch devices */ 109 | @media (hover: hover) and (pointer: fine) { 110 | a.primary:hover { 111 | background: var(--button-primary-hover); 112 | border-color: transparent; 113 | } 114 | 115 | a.secondary:hover { 116 | background: var(--button-secondary-hover); 117 | border-color: transparent; 118 | } 119 | 120 | .footer a:hover { 121 | text-decoration: underline; 122 | text-underline-offset: 4px; 123 | } 124 | } 125 | 126 | @media (max-width: 600px) { 127 | .page { 128 | padding: 32px; 129 | padding-bottom: 80px; 130 | } 131 | 132 | .main { 133 | align-items: center; 134 | } 135 | 136 | .main ol { 137 | text-align: center; 138 | } 139 | 140 | .ctas { 141 | flex-direction: column; 142 | } 143 | 144 | .ctas a { 145 | font-size: 14px; 146 | height: 40px; 147 | padding: 0 16px; 148 | } 149 | 150 | a.secondary { 151 | min-width: auto; 152 | } 153 | 154 | .footer { 155 | flex-wrap: wrap; 156 | align-items: center; 157 | justify-content: center; 158 | } 159 | } 160 | 161 | @media (prefers-color-scheme: dark) { 162 | .logo { 163 | filter: invert(); 164 | } 165 | } 166 | 167 | .container { 168 | max-width: 800px; 169 | margin: 0 auto; 170 | padding: 20px; 171 | font-family: Arial, sans-serif; 172 | } 173 | 174 | .form { 175 | display: flex; 176 | margin-bottom: 20px; 177 | } 178 | 179 | .input { 180 | flex-grow: 1; 181 | padding: 10px; 182 | font-size: 16px; 183 | border: 1px solid #ccc; 184 | border-radius: 4px 0 0 4px; 185 | } 186 | 187 | .button { 188 | padding: 10px 20px; 189 | font-size: 16px; 190 | background-color: #0070f3; 191 | color: white; 192 | border: none; 193 | border-radius: 0 4px 4px 0; 194 | cursor: pointer; 195 | transition: background-color 0.3s ease; 196 | } 197 | 198 | .button:hover { 199 | background-color: #0051a2; 200 | } 201 | 202 | .progress { 203 | background-color: #f0f0f0; 204 | padding: 20px; 205 | border-radius: 4px; 206 | } 207 | 208 | .message { 209 | margin-bottom: 10px; 210 | margin-top: 10px; 211 | padding: 10px; 212 | background-color: white; 213 | border-radius: 4px; 214 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 215 | } 216 | 217 | .responseButtons { 218 | margin-top: 10px; 219 | } 220 | 221 | .responseButtons button { 222 | margin-top: 10px; 223 | margin-right: 10px; 224 | border-radius: 4px; 225 | } 226 | -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from 'react'; 3 | import styles from './page.module.css'; // Add this import 4 | 5 | declare global { 6 | interface Window { 7 | socket?: WebSocket; 8 | } 9 | } 10 | 11 | export default function Home() { 12 | const [query, setQuery] = useState(''); 13 | const [progress, setProgress] = useState([]); 14 | const [responseRequired, setResponseRequired] = useState(false); 15 | 16 | useEffect(() => { 17 | return () => { 18 | // Clean up WebSocket connection if component unmounts 19 | if (window.socket) { 20 | window.socket.close(); 21 | } 22 | }; 23 | }, []); 24 | 25 | const handleSubmit = (e: React.FormEvent) => { 26 | e.preventDefault(); 27 | setProgress([]); // Clear previous progress 28 | const socket = new WebSocket('ws://localhost:8000/query'); 29 | 30 | socket.onopen = () => { 31 | socket.send(JSON.stringify({ question: query })); 32 | }; 33 | 34 | socket.onmessage = (event) => { 35 | const data = JSON.parse(event.data); 36 | console.log('Received:', data); 37 | if (data.type === 'progress') { 38 | setProgress(prev => [...prev, data.payload]); 39 | } else if (data.type === 'input_required') { 40 | setProgress(prev => [...prev, data.payload]); 41 | setResponseRequired(true); 42 | } else if (data.type === 'final_result') { 43 | setProgress(prev => [...prev, `Final result: ${data.payload}`]); 44 | } 45 | }; 46 | 47 | socket.onerror = (error) => { 48 | console.error('WebSocket Error:', error); 49 | setProgress(prev => [...prev, `Error: ${error}`]); 50 | }; 51 | 52 | socket.onclose = () => { 53 | console.log('WebSocket connection closed'); 54 | setProgress(prev => [...prev, 'Connection closed']); 55 | }; 56 | 57 | // Store the socket in the window object for cleanup 58 | window.socket = socket; 59 | }; 60 | 61 | const handleResponse = (response: 'yes' | 'no') => { 62 | if (window.socket && window.socket.readyState === WebSocket.OPEN) { 63 | window.socket.send(JSON.stringify({ response })); 64 | setResponseRequired(false); 65 | } else { 66 | console.error('WebSocket is not connected'); 67 | setProgress(prev => [...prev, 'Error: WebSocket is not connected']); 68 | } 69 | }; 70 | 71 | return ( 72 |
73 |
74 | setQuery(e.target.value)} 78 | placeholder="Enter your query" 79 | className={styles.input} 80 | /> 81 | 82 |
83 |
84 |

Progress:

85 | {progress.map((message, index) => ( 86 |
87 |

{message}

88 | {responseRequired && index === progress.length - 1 && ( 89 |
90 |

Does this look like enough to write a report?

91 | 92 | 93 |
94 | )} 95 |
96 | ))} 97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------