├── .eslintrc.json
├── app
├── favicon.ico
├── api
│ ├── humanloop_client.ts
│ ├── feedback
│ │ └── route.ts
│ └── chat
│ │ └── route.ts
├── layout.tsx
├── globals.css
└── page.tsx
├── next.config.js
├── postcss.config.js
├── .gitignore
├── tailwind.config.js
├── public
├── vercel.svg
└── next.svg
├── package.json
├── tsconfig.json
└── README.md
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/humanloop/hl-chatgpt-clone-typescript/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/app/api/humanloop_client.ts:
--------------------------------------------------------------------------------
1 | import { HumanloopClient } from "humanloop";
2 |
3 | if (!process.env.HUMANLOOP_API_KEY) {
4 | throw Error(
5 | "no Humanloop API key provided; add one to your .env.local file with: `HUMANLOOP_API_KEY=...",
6 | );
7 | }
8 |
9 | export const humanloop = new HumanloopClient({
10 | environment: "https://api.humanloop.com/v5",
11 | apiKey: process.env.HUMANLOOP_API_KEY,
12 | });
13 |
--------------------------------------------------------------------------------
/.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 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12 | 'gradient-conic':
13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | }
19 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import { Inter } from "next/font/google";
3 |
4 | const inter = Inter({ subsets: ["latin"] });
5 |
6 | export const metadata = {
7 | title: "ChatGPT | Humanloop Clone",
8 | description:
9 | "A clone of ChatGPT using Humanloop as the backend for logging, model evaluation and improvement.",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | return (
18 |
19 |
{children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hl-chatgpt-clone-typescript",
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 | "@types/node": "20.11.26",
13 | "@types/react": "18.2.65",
14 | "@types/react-dom": "18.2.21",
15 | "autoprefixer": "10.4.18",
16 | "eslint": "8.57.0",
17 | "eslint-config-next": "14.1.3",
18 | "humanloop": "^0.8.7",
19 | "next": "14.1.3",
20 | "postcss": "8.4.35",
21 | "react": "18.2.0",
22 | "react-dom": "18.2.0",
23 | "tailwindcss": "3.4.1",
24 | "typescript": "5.4.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/api/feedback/route.ts:
--------------------------------------------------------------------------------
1 | import { HumanloopClient } from "humanloop";
2 | import { humanloop } from "../humanloop_client";
3 |
4 | const PATH_TO_EVALUATOR = "Example Evaluators/Human/rating";
5 | export async function POST(req: Request): Promise {
6 | const body = await req.json();
7 | const response = await humanloop.evaluators.log({
8 | parentId: body.logId,
9 | path: PATH_TO_EVALUATOR,
10 | judgment: body.judgment,
11 | spec : {
12 | argumentsType: "target_free",
13 | returnType: "select",
14 | evaluatorType: "human",
15 | options: [{"name": "bad", "valence": "negative"}, {"name": "good", "valence": "positive"}]
16 |
17 | }
18 |
19 | });
20 | return new Response();
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is the complete source code repository for the Humanloop [Add user feedback tutorial](https://docs.humanloop.com/docs/tutorials/capture-user-feedback).
2 |
3 | ## Getting Started
4 |
5 | Clone the repository and run `npm i` to install dependencies.
6 |
7 | Add a `.env.local` file at the root of the project and include your Humanloop API key:
8 |
9 | ```
10 | HUMANLOOP_API_KEY=...
11 | # Optionally setup OpenAI API key, if you haven't already
12 | OPENAI_API_KEY=
13 | ```
14 |
15 | Run the development server:
16 |
17 | ```bash
18 | npm run dev
19 | # or
20 | yarn dev
21 | # or
22 | pnpm dev
23 | ```
24 |
25 | Open [http://localhost:3000](http://localhost:3000) in your browser to see the result.
26 |
27 | ## Learn More
28 |
29 | To learn more about using Humanloop, check out the tutorials and guides in our [documentation](https://docs.humanloop.com/).
30 |
31 | To learn more about Next.js, take a look at the following resources:
32 |
33 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
34 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
35 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { readableStreamAsyncIterable } from "humanloop/core/streaming-fetcher/Stream";
2 | import { humanloop } from "../humanloop_client";
3 |
4 | export const PROMPT_HUMANLOOP_PATH =
5 | "chatgpt-clone-tutorial/customer-support-agent";
6 |
7 | export async function POST(req: Request): Promise {
8 | const messages = await req.json();
9 |
10 | const response = await humanloop.prompts.callStream({
11 | path: PROMPT_HUMANLOOP_PATH,
12 | prompt: {
13 | model: "gpt-4",
14 | template: [
15 | {
16 | role: "system",
17 | content: "You are a helpful assistant.",
18 | },
19 | ],
20 | },
21 | messages,
22 | // This is the name of your company. You can change it to any string you like.
23 | // It matches the companyName input defined in the Prompt Version template.
24 | inputs: {
25 | companyName: "Acme Co.",
26 | },
27 | providerApiKeys: {
28 | openai: process.env.OPENAI_API_KEY,
29 | },
30 | });
31 |
32 | const stream = readableStreamAsyncIterable(response);
33 |
34 | // Create a ReadableStream from the async iterable
35 | const readableStream = new ReadableStream({
36 | async start(controller) {
37 | try {
38 | for await (const chunk of stream) {
39 | // Add a newline delimiter between chunks
40 | const serializedChunk = JSON.stringify(chunk) + "\n";
41 | controller.enqueue(new TextEncoder().encode(serializedChunk));
42 | }
43 | controller.close();
44 | } catch (error) {
45 | controller.error(error);
46 | }
47 | },
48 | });
49 |
50 | return new Response(readableStream, {
51 | headers: { "Content-Type": "application/json" },
52 | });
53 | }
54 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ChatMessage, ChatRole } from "humanloop/api";
4 | import React, { useState } from "react";
5 |
6 | interface Message {
7 | // we capture logId only for the assistant messages
8 | logId?: string;
9 | message: ChatMessage;
10 | }
11 |
12 | export default function Home() {
13 | const [messages, setMessages] = useState([]);
14 | const [userInputValue, setUserInputValue] = useState("");
15 | const [showToast, setShowToast] = useState(false);
16 | const [toastMessage, setToastMessage] = useState("");
17 |
18 | const onSend = async () => {
19 | const userMessage: ChatMessage = {
20 | role: "user",
21 | content: userInputValue,
22 | };
23 |
24 | setUserInputValue("");
25 |
26 | const newMessages = [
27 | ...messages,
28 | { message: userMessage },
29 | { message: { role: ChatRole.Assistant, content: "" } },
30 | ];
31 |
32 | setMessages(newMessages);
33 |
34 | const response = await fetch("/api/chat", {
35 | method: "POST",
36 | headers: {
37 | "Content-Type": "application/json",
38 | },
39 | body: JSON.stringify(newMessages.map((msg) => msg.message)),
40 | });
41 |
42 | if (!response.body) return;
43 | const reader = response.body.getReader();
44 | const decoder = new TextDecoder();
45 |
46 | try {
47 | while (true) {
48 | const { value, done } = await reader.read();
49 | if (done) break;
50 |
51 | const text = decoder.decode(value);
52 | const chunks = text.split("\n").filter((chunk) => chunk.trim());
53 |
54 | for (const chunk of chunks) {
55 | try {
56 | const parsed = JSON.parse(chunk);
57 | setMessages((messages) => {
58 | const updatedLastMessage = messages.slice(-1)[0];
59 | return [
60 | ...messages.slice(0, -1),
61 | {
62 | ...updatedLastMessage,
63 | logId: parsed.id,
64 | message: {
65 | role: ChatRole.Assistant,
66 | content:
67 | (updatedLastMessage.message.content as string) +
68 | parsed.output,
69 | },
70 | },
71 | ];
72 | });
73 | } catch (e) {
74 | console.error("Failed to parse chunk:", chunk);
75 | }
76 | }
77 | }
78 | } finally {
79 | reader.releaseLock();
80 | }
81 | };
82 |
83 | const handleKeyDown = (e: React.KeyboardEvent) => {
84 | if (e.key === "Enter") {
85 | onSend();
86 | }
87 | };
88 |
89 | const showFeedbackToast = (feedback: string) => {
90 | setToastMessage(
91 | feedback === "good"
92 | ? "👍 Captured your feedback!"
93 | : "👎 Captured your feedback!",
94 | );
95 | setShowToast(true);
96 | setTimeout(() => setShowToast(false), 3000);
97 | };
98 |
99 | return (
100 |
101 | {showToast && (
102 |
103 | {toastMessage}
104 |
105 | )}
106 |
107 | Customer Support Chat
108 |
109 |
110 |
111 | {messages.map((msg, idx) => (
112 |
118 | ))}
119 |
120 |
121 |
122 | User
123 |
124 |
setUserInputValue(e.target.value)}
131 | onKeyDown={(e) => handleKeyDown(e)}
132 | >
133 |
139 |
140 |
141 |
142 | );
143 | }
144 |
145 | const MessageRow: React.FC<
146 | Message & { showFeedbackToast: (feedback: string) => void }
147 | > = ({ message, logId, showFeedbackToast }) => {
148 | const captureUserFeedback = async (logId: string, feedback: string) => {
149 | const response = await fetch("/api/feedback", {
150 | method: "POST",
151 | headers: {
152 | "Content-Type": "application/json",
153 | },
154 | body: JSON.stringify({ logId: logId, judgment: feedback }),
155 | });
156 |
157 | showFeedbackToast(feedback);
158 | };
159 |
160 | return (
161 |
162 |
163 | {message.role}
164 |
165 | {message.content ? (
166 |
167 | {message.content as string}
168 |
169 | ) : (
170 |
...
171 | )}
172 | {/* {logId && (
173 |
174 |
182 |
190 |
191 | )} */}
192 |
193 | );
194 | };
195 |
--------------------------------------------------------------------------------