├── .gitignore ├── manifest.json ├── widget-src ├── tsconfig.json ├── MessageRow.tsx └── code.tsx ├── .prettierrc ├── package.json ├── README.md ├── ui.html └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | *.log 3 | *.log.* 4 | node_modules 5 | 6 | out/ 7 | dist/ 8 | code.js 9 | 10 | # macOS 11 | .DS_Store -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FigChat", 3 | "id": "1224228637658454666", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "capabilities": [], 7 | "enableProposedApi": false, 8 | "editorType": ["figjam", "figma"], 9 | "containsWidget": true, 10 | "widgetApi": "1.0.0", 11 | "ui": "ui.html" 12 | } 13 | -------------------------------------------------------------------------------- /widget-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "jsxFactory": "figma.widget.h", 5 | "jsxFragmentFactory": "figma.widget.Fragment", 6 | "target": "es6", 7 | "lib": ["es2016"], 8 | "strict": true, 9 | "typeRoots": ["../node_modules/@types", "../node_modules/@figma"], 10 | "moduleResolution": "nodenext", 11 | "allowSyntheticDefaultImports": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": false, 4 | "embeddedLanguageFormatting": "auto", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 80, 10 | "proseWrap": "preserve", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": false, 14 | "singleQuote": true, 15 | "tabWidth": 4, 16 | "trailingComma": "none", 17 | "useTabs": true, 18 | "vueIndentScriptAndStyle": false, 19 | "jsdocParser": true 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fable-chat", 3 | "version": "1.0.0", 4 | "description": "ChatGPT on a multiplayer canvas", 5 | "scripts": { 6 | "build": "esbuild widget-src/code.tsx --bundle --outfile=dist/code.js --target=es6", 7 | "tsc": "tsc --noEmit -p widget-src", 8 | "watch": "npm run build -- --watch", 9 | "dev": "npm run build -- --watch" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@figma/plugin-typings": "*", 15 | "@figma/widget-typings": "*", 16 | "esbuild": "*", 17 | "typescript": "*" 18 | }, 19 | "dependencies": { 20 | "axios": "^1.3.4", 21 | "nanoid": "^4.0.2", 22 | "openai": "^3.2.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Below are the steps to get your widget running. You can also find instructions at: 2 | 3 | https://www.figma.com/widget-docs/setup-guide/ 4 | 5 | This widget template uses Typescript and NPM, two standard tools in creating JavaScript applications. 6 | 7 | First, download Node.js which comes with NPM. This will allow you to install TypeScript and other 8 | libraries. You can find the download link here: 9 | 10 | https://nodejs.org/en/download/ 11 | 12 | Next, install TypeScript, esbuild and the latest type definitions by running: 13 | 14 | npm install 15 | 16 | If you are familiar with JavaScript, TypeScript will look very familiar. In fact, valid JavaScript code 17 | is already valid Typescript code. 18 | 19 | TypeScript adds type annotations to variables. This allows code editors such as Visual Studio Code 20 | to provide information about the Figma API while you are writing code, as well as help catch bugs 21 | you previously didn't notice. 22 | 23 | For more information, visit https://www.typescriptlang.org/ 24 | 25 | Using TypeScript requires a compiler to convert TypeScript (widget-src/code.tsx) into JavaScript (dist/code.js) 26 | for the browser to run. We use esbuild to do this for us. 27 | 28 | We recommend writing TypeScript code using Visual Studio code: 29 | 30 | 1. Download Visual Studio Code if you haven't already: https://code.visualstudio.com/. 31 | 2. Open this directory in Visual Studio Code. 32 | 3. Compile TypeScript to JavaScript: Run the "Terminal > Run Build Task..." menu item, 33 | then select "npm: watch". You will have to do this again every time 34 | you reopen Visual Studio Code. 35 | 36 | That's it! Visual Studio Code will regenerate the JavaScript file every time you save. 37 | -------------------------------------------------------------------------------- /widget-src/MessageRow.tsx: -------------------------------------------------------------------------------- 1 | const {widget} = figma 2 | const {AutoLayout, Text, Input, Line, SVG} = widget 3 | 4 | import {ChatMessage} from './code' 5 | 6 | type MessageRowProps = { 7 | message: ChatMessage 8 | index?: number 9 | expandable?: boolean 10 | deleteable?: boolean 11 | placeholder?: string 12 | widened?: boolean 13 | roleWidth?: number 14 | roleColor?: string 15 | monospace?: boolean 16 | 17 | onDelete?: () => void 18 | onExpandCollapse?: (expanded: boolean) => void 19 | onToggleRole?: (role: 'user' | 'assistant') => void 20 | onUpdateContent?: (content: string) => void 21 | } 22 | 23 | export const MessageRow = ({ 24 | message, 25 | index, 26 | expandable, 27 | deleteable, 28 | placeholder, 29 | widened, 30 | roleWidth, 31 | roleColor = '#000000', 32 | monospace = false, 33 | 34 | onDelete, 35 | onExpandCollapse, 36 | onToggleRole, 37 | onUpdateContent 38 | }: MessageRowProps) => { 39 | return ( 40 | 41 | 47 | 48 | {expandable && ( 49 | 63 | onExpandCollapse?.(!message.collapsed) 64 | } 65 | > 66 | ` 70 | : `` 71 | } 72 | opacity={0.2} 73 | /> 74 | 75 | )} 76 | 89 | onToggleRole?.( 90 | message.role === 'assistant' 91 | ? 'user' 92 | : 'assistant' 93 | ) 94 | : undefined 95 | } 96 | cornerRadius={4} 97 | width="hug-contents" 98 | > 99 | 111 | {message.role} 112 | 113 | 114 | 115 | 123 | {message.collapsed && ( 124 | onExpandCollapse?.(true)} 131 | fill={{r: 0, g: 0, b: 0, a: 0.5}} 132 | fontFamily={monospace ? 'Roboto Mono' : 'Inter'} 133 | > 134 | {message.content 135 | .split('') 136 | .slice(0, widened ? 70 : 30) 137 | .join('') + '...'} 138 | 139 | )} 140 | {!message.collapsed && ( 141 | { 147 | onUpdateContent?.(e.characters) 148 | }} 149 | placeholder={placeholder} 150 | height={!message.collapsed ? 'hug-contents' : 23} 151 | inputBehavior="multiline" 152 | paragraphSpacing={8} 153 | fontFamily={monospace ? 'Roboto Mono' : 'Inter'} 154 | /> 155 | )} 156 | 157 | {deleteable && ( 158 | 173 | `} 175 | opacity={0.2} 176 | /> 177 | 178 | )} 179 | 180 | 186 | 187 | ) 188 | } 189 | -------------------------------------------------------------------------------- /ui.html: -------------------------------------------------------------------------------- 1 | 230 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@esbuild/android-arm64@0.17.15": 6 | version "0.17.15" 7 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.15.tgz#893ad71f3920ccb919e1757c387756a9bca2ef42" 8 | integrity sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA== 9 | 10 | "@esbuild/android-arm@0.17.15": 11 | version "0.17.15" 12 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.15.tgz#143e0d4e4c08c786ea410b9a7739779a9a1315d8" 13 | integrity sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg== 14 | 15 | "@esbuild/android-x64@0.17.15": 16 | version "0.17.15" 17 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.15.tgz#d2d12a7676b2589864281b2274355200916540bc" 18 | integrity sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ== 19 | 20 | "@esbuild/darwin-arm64@0.17.15": 21 | version "0.17.15" 22 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.15.tgz#2e88e79f1d327a2a7d9d06397e5232eb0a473d61" 23 | integrity sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA== 24 | 25 | "@esbuild/darwin-x64@0.17.15": 26 | version "0.17.15" 27 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.15.tgz#9384e64c0be91388c57be6d3a5eaf1c32a99c91d" 28 | integrity sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg== 29 | 30 | "@esbuild/freebsd-arm64@0.17.15": 31 | version "0.17.15" 32 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.15.tgz#2ad5a35bc52ebd9ca6b845dbc59ba39647a93c1a" 33 | integrity sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg== 34 | 35 | "@esbuild/freebsd-x64@0.17.15": 36 | version "0.17.15" 37 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.15.tgz#b513a48446f96c75fda5bef470e64d342d4379cd" 38 | integrity sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ== 39 | 40 | "@esbuild/linux-arm64@0.17.15": 41 | version "0.17.15" 42 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.15.tgz#9697b168175bfd41fa9cc4a72dd0d48f24715f31" 43 | integrity sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA== 44 | 45 | "@esbuild/linux-arm@0.17.15": 46 | version "0.17.15" 47 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.15.tgz#5b22062c54f48cd92fab9ffd993732a52db70cd3" 48 | integrity sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw== 49 | 50 | "@esbuild/linux-ia32@0.17.15": 51 | version "0.17.15" 52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.15.tgz#eb28a13f9b60b5189fcc9e98e1024f6b657ba54c" 53 | integrity sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q== 54 | 55 | "@esbuild/linux-loong64@0.17.15": 56 | version "0.17.15" 57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.15.tgz#32454bdfe144cf74b77895a8ad21a15cb81cfbe5" 58 | integrity sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ== 59 | 60 | "@esbuild/linux-mips64el@0.17.15": 61 | version "0.17.15" 62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.15.tgz#af12bde0d775a318fad90eb13a0455229a63987c" 63 | integrity sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ== 64 | 65 | "@esbuild/linux-ppc64@0.17.15": 66 | version "0.17.15" 67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.15.tgz#34c5ed145b2dfc493d3e652abac8bd3baa3865a5" 68 | integrity sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg== 69 | 70 | "@esbuild/linux-riscv64@0.17.15": 71 | version "0.17.15" 72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.15.tgz#87bd515e837f2eb004b45f9e6a94dc5b93f22b92" 73 | integrity sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA== 74 | 75 | "@esbuild/linux-s390x@0.17.15": 76 | version "0.17.15" 77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.15.tgz#20bf7947197f199ddac2ec412029a414ceae3aa3" 78 | integrity sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg== 79 | 80 | "@esbuild/linux-x64@0.17.15": 81 | version "0.17.15" 82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.15.tgz#31b93f9c94c195e852c20cd3d1914a68aa619124" 83 | integrity sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg== 84 | 85 | "@esbuild/netbsd-x64@0.17.15": 86 | version "0.17.15" 87 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.15.tgz#8da299b3ac6875836ca8cdc1925826498069ac65" 88 | integrity sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA== 89 | 90 | "@esbuild/openbsd-x64@0.17.15": 91 | version "0.17.15" 92 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.15.tgz#04a1ec3d4e919714dba68dcf09eeb1228ad0d20c" 93 | integrity sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w== 94 | 95 | "@esbuild/sunos-x64@0.17.15": 96 | version "0.17.15" 97 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.15.tgz#6694ebe4e16e5cd7dab6505ff7c28f9c1c695ce5" 98 | integrity sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ== 99 | 100 | "@esbuild/win32-arm64@0.17.15": 101 | version "0.17.15" 102 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.15.tgz#1f95b2564193c8d1fee8f8129a0609728171d500" 103 | integrity sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q== 104 | 105 | "@esbuild/win32-ia32@0.17.15": 106 | version "0.17.15" 107 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.15.tgz#c362b88b3df21916ed7bcf75c6d09c6bf3ae354a" 108 | integrity sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w== 109 | 110 | "@esbuild/win32-x64@0.17.15": 111 | version "0.17.15" 112 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz#c2e737f3a201ebff8e2ac2b8e9f246b397ad19b8" 113 | integrity sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA== 114 | 115 | "@figma/plugin-typings@*": 116 | version "1.62.0" 117 | resolved "https://registry.yarnpkg.com/@figma/plugin-typings/-/plugin-typings-1.62.0.tgz#b3d416ce0a8df6c8b611e091b960ab79fbaeaf66" 118 | integrity sha512-Pv1DZqsuJJbzVsqgCz1fPPxEdQ1o521jbcMw1MDyM9oerw7QltM8TluR5BrGgBi6Zc8zXE6J6ZuUbzz/x7Bh6A== 119 | 120 | "@figma/widget-typings@*": 121 | version "1.5.0" 122 | resolved "https://registry.yarnpkg.com/@figma/widget-typings/-/widget-typings-1.5.0.tgz#ac56637b4cab229c4f19aa1a4ceb63cbf60373f0" 123 | integrity sha512-gktog9TSdFpFIu3mEK3e8EsAUhBOmfVzHVMACR/0DRCneXUNaFyQnr/0Layg6BvAUyYzcYv8jT7Oe2e7GHIQ4g== 124 | 125 | asynckit@^0.4.0: 126 | version "0.4.0" 127 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 128 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 129 | 130 | axios@^0.26.0: 131 | version "0.26.1" 132 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" 133 | integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== 134 | dependencies: 135 | follow-redirects "^1.14.8" 136 | 137 | axios@^1.3.4: 138 | version "1.3.4" 139 | resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" 140 | integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== 141 | dependencies: 142 | follow-redirects "^1.15.0" 143 | form-data "^4.0.0" 144 | proxy-from-env "^1.1.0" 145 | 146 | combined-stream@^1.0.8: 147 | version "1.0.8" 148 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 149 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 150 | dependencies: 151 | delayed-stream "~1.0.0" 152 | 153 | delayed-stream@~1.0.0: 154 | version "1.0.0" 155 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 156 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 157 | 158 | esbuild@*: 159 | version "0.17.15" 160 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.15.tgz#209ebc87cb671ffb79574db93494b10ffaf43cbc" 161 | integrity sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw== 162 | optionalDependencies: 163 | "@esbuild/android-arm" "0.17.15" 164 | "@esbuild/android-arm64" "0.17.15" 165 | "@esbuild/android-x64" "0.17.15" 166 | "@esbuild/darwin-arm64" "0.17.15" 167 | "@esbuild/darwin-x64" "0.17.15" 168 | "@esbuild/freebsd-arm64" "0.17.15" 169 | "@esbuild/freebsd-x64" "0.17.15" 170 | "@esbuild/linux-arm" "0.17.15" 171 | "@esbuild/linux-arm64" "0.17.15" 172 | "@esbuild/linux-ia32" "0.17.15" 173 | "@esbuild/linux-loong64" "0.17.15" 174 | "@esbuild/linux-mips64el" "0.17.15" 175 | "@esbuild/linux-ppc64" "0.17.15" 176 | "@esbuild/linux-riscv64" "0.17.15" 177 | "@esbuild/linux-s390x" "0.17.15" 178 | "@esbuild/linux-x64" "0.17.15" 179 | "@esbuild/netbsd-x64" "0.17.15" 180 | "@esbuild/openbsd-x64" "0.17.15" 181 | "@esbuild/sunos-x64" "0.17.15" 182 | "@esbuild/win32-arm64" "0.17.15" 183 | "@esbuild/win32-ia32" "0.17.15" 184 | "@esbuild/win32-x64" "0.17.15" 185 | 186 | follow-redirects@^1.14.8, follow-redirects@^1.15.0: 187 | version "1.15.2" 188 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" 189 | integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== 190 | 191 | form-data@^4.0.0: 192 | version "4.0.0" 193 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 194 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 195 | dependencies: 196 | asynckit "^0.4.0" 197 | combined-stream "^1.0.8" 198 | mime-types "^2.1.12" 199 | 200 | mime-db@1.52.0: 201 | version "1.52.0" 202 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 203 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 204 | 205 | mime-types@^2.1.12: 206 | version "2.1.35" 207 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 208 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 209 | dependencies: 210 | mime-db "1.52.0" 211 | 212 | nanoid@^4.0.2: 213 | version "4.0.2" 214 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e" 215 | integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw== 216 | 217 | openai@^3.2.1: 218 | version "3.2.1" 219 | resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866" 220 | integrity sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A== 221 | dependencies: 222 | axios "^0.26.0" 223 | form-data "^4.0.0" 224 | 225 | proxy-from-env@^1.1.0: 226 | version "1.1.0" 227 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 228 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 229 | 230 | typescript@*: 231 | version "5.0.3" 232 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.3.tgz#fe976f0c826a88d0a382007681cbb2da44afdedf" 233 | integrity sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA== 234 | -------------------------------------------------------------------------------- /widget-src/code.tsx: -------------------------------------------------------------------------------- 1 | import {MessageRow} from './MessageRow' 2 | 3 | const {widget} = figma 4 | const { 5 | AutoLayout, 6 | Text, 7 | Input, 8 | Line, 9 | SVG, 10 | usePropertyMenu, 11 | useEffect, 12 | waitForTask, 13 | useSyncedState 14 | } = widget 15 | 16 | export type ChatMessage = { 17 | role: string 18 | content: string 19 | collapsed: boolean 20 | } 21 | 22 | const SampleMessages: ChatMessage[] = [ 23 | {role: 'user', content: 'Hi', collapsed: false}, 24 | {role: 'assistant', content: 'Hello!', collapsed: false} 25 | ] 26 | 27 | function FigChat() { 28 | const [messages, setMessages] = useSyncedState( 29 | 'messages', 30 | SampleMessages 31 | ) 32 | 33 | const [titleVisible, setTitleVisible] = useSyncedState( 34 | 'titleVisible', 35 | false 36 | ) 37 | const [title, setTitle] = useSyncedState('title', '') 38 | 39 | const [systemMessageVisible, setSystemMessageVisible] = 40 | useSyncedState('systemMessageVisible', false) 41 | const [systemMessage, setSystemMessage] = useSyncedState< 42 | ChatMessage & { 43 | role: 'system' 44 | } 45 | >('systemMessage', {role: 'system', content: '', collapsed: false}) 46 | 47 | const [keyVisible, setKeyVisible] = useSyncedState( 48 | 'keyVisible', 49 | false 50 | ) 51 | const [openAIKeyMessage, setOpenAIKeyMessage] = useSyncedState< 52 | ChatMessage & { 53 | role: 'OpenAI Key' 54 | } 55 | >('key', {role: 'OpenAI Key', content: '', collapsed: false}) 56 | const [anthropicKeyMessage, setAnthropicKeyMessage] = useSyncedState< 57 | ChatMessage & { 58 | role: 'Anthropic Key' 59 | } 60 | >('anthropicKey', {role: 'Anthropic Key', content: '', collapsed: false}) 61 | const [anthropicProxyURLMessage, setAnthropicProxyURLMessage] = 62 | useSyncedState('anthropicProxyURL', { 63 | role: 'Proxy URL', 64 | content: 'http://localhost:5555', 65 | collapsed: false 66 | }) 67 | 68 | const [loadState, setLoadState] = useSyncedState< 69 | 'ready' | 'loading' | 'error' 70 | >('loadState', 'ready') 71 | const [error, setError] = useSyncedState>( 72 | 'error', 73 | {} 74 | ) 75 | const widgetId = figma.widget.useWidgetId() 76 | 77 | // This is used to detect if this widget node is new or not 78 | // If the node ID does not match the one in the synced state, then this is a new node, and we should fetch keys 79 | const [storedWidgetId, setStoredWidgetId] = useSyncedState( 80 | 'storedWidgetId', 81 | '' 82 | ) 83 | 84 | const [model, setModel] = useSyncedState('model', 'gpt-4') 85 | const [temp, setTemp] = useSyncedState('temp', '0.7') 86 | const [topP, setTopP] = useSyncedState('top_p', '1') 87 | const [widened, setWidened] = useSyncedState('widened', false) 88 | 89 | // Do not use colorState directly. Use color() instead. 90 | // #ffffff is used as a special value to indicate "no color". 91 | const [colorState, setColorState] = useSyncedState('color', '#ffffff') 92 | 93 | const isGPT = () => model.startsWith('gpt') 94 | const color = () => (colorState === '#ffffff' ? undefined : colorState) 95 | 96 | // Key Fetch 97 | // Fetch OpenAI & Anthropic keys from existing widgets on widget mount 98 | const keyFetch = () => { 99 | // Detect OpenAI Key 100 | let openAIKey = openAIKeyMessage.content 101 | let anthropicKey = anthropicKeyMessage.content 102 | let anthropicProxyURL = anthropicProxyURLMessage.content 103 | 104 | // Fetch keys if needed 105 | if (!openAIKey || !anthropicKey || !anthropicProxyURL) { 106 | let existingOpenAIKey: string | undefined = undefined 107 | let existingAnthropicKey: string | undefined = undefined 108 | let existingAnthropicProxyURL: string | undefined = undefined 109 | 110 | figma.currentPage.children.forEach((node) => { 111 | if ( 112 | node.type === 'WIDGET' && 113 | node.widgetId === figma.widgetId 114 | ) { 115 | const nodeOpenAIKey = node.widgetSyncedState.key?.content 116 | if (nodeOpenAIKey) existingOpenAIKey = nodeOpenAIKey 117 | 118 | const nodeAnthropicKey = 119 | node.widgetSyncedState.anthropicKey?.content 120 | if (nodeAnthropicKey) 121 | existingAnthropicKey = nodeAnthropicKey 122 | 123 | const nodeAnthropicProxyURL = 124 | node.widgetSyncedState.anthropicProxyURL?.content 125 | if (nodeAnthropicProxyURL) 126 | existingAnthropicProxyURL = nodeAnthropicProxyURL 127 | } 128 | }) 129 | 130 | // Set OpenAI key if found 131 | if (!openAIKey && existingOpenAIKey) { 132 | setOpenAIKeyMessage({ 133 | role: 'OpenAI Key', 134 | content: existingOpenAIKey, 135 | collapsed: false 136 | }) 137 | openAIKey = existingOpenAIKey 138 | } 139 | 140 | // Set Anthropic key if found 141 | if (!anthropicKey && existingAnthropicKey) { 142 | setAnthropicKeyMessage({ 143 | role: 'Anthropic Key', 144 | content: existingAnthropicKey, 145 | collapsed: false 146 | }) 147 | anthropicKey = existingAnthropicKey 148 | } 149 | 150 | // Set Anthropic proxy URL if found 151 | if (!anthropicProxyURL && existingAnthropicProxyURL) { 152 | console.log('setting proxy url') 153 | debugger 154 | setAnthropicProxyURLMessage({ 155 | role: 'Proxy URL', 156 | content: existingAnthropicProxyURL, 157 | collapsed: false 158 | }) 159 | anthropicProxyURL = existingAnthropicProxyURL 160 | } 161 | } 162 | 163 | // Return keys 164 | return { 165 | openAIKey, 166 | anthropicKey, 167 | anthropicProxyURL 168 | } 169 | } 170 | 171 | // Key Fetch on Mount Effect 172 | // Run Key Fetch on widget mount 173 | useEffect(() => { 174 | if (widgetId !== storedWidgetId) { 175 | // New widget node, fetch keys 176 | setStoredWidgetId(widgetId) 177 | keyFetch() 178 | } 179 | }) 180 | 181 | usePropertyMenu( 182 | [ 183 | { 184 | propertyName: 'model', 185 | itemType: 'dropdown', 186 | tooltip: 'Model', 187 | options: [ 188 | { 189 | option: 'gpt-4', 190 | label: 'GPT-4' 191 | }, 192 | { 193 | option: 'gpt-3.5-turbo-16k', 194 | label: 'GPT-3.5' 195 | }, 196 | { 197 | option: 'claude-v1-100k', 198 | label: 'Claude v1' 199 | }, 200 | { 201 | option: 'claude-instant-v1-100k', 202 | label: 'Claude Instant v1' 203 | }, 204 | { 205 | option: 'claude-2', 206 | label: 'Claude v2' 207 | } 208 | ], 209 | selectedOption: model 210 | }, 211 | {itemType: 'separator'}, 212 | { 213 | propertyName: 'temp', 214 | itemType: 'dropdown', 215 | tooltip: 'Temp', 216 | options: [ 217 | { 218 | option: '0', 219 | label: '0' 220 | }, 221 | { 222 | option: '0.1', 223 | label: '0.1' 224 | }, 225 | { 226 | option: '0.2', 227 | label: '0.2' 228 | }, 229 | { 230 | option: '0.3', 231 | label: '0.3' 232 | }, 233 | { 234 | option: '0.4', 235 | label: '0.4' 236 | }, 237 | { 238 | option: '0.5', 239 | label: '0.5' 240 | }, 241 | { 242 | option: '0.6', 243 | label: '0.6' 244 | }, 245 | 246 | { 247 | option: '0.7', 248 | label: '0.7' 249 | }, 250 | { 251 | option: '0.8', 252 | label: '0.8' 253 | }, 254 | { 255 | option: '0.9', 256 | 257 | label: '0.9' 258 | }, 259 | { 260 | option: '1', 261 | label: '1' 262 | } 263 | ], 264 | selectedOption: temp 265 | }, 266 | { 267 | propertyName: 'top_p', 268 | itemType: 'dropdown', 269 | tooltip: 'Top P', 270 | options: [ 271 | { 272 | option: '0', 273 | label: '0' 274 | }, 275 | { 276 | option: '0.1', 277 | label: '0.1' 278 | }, 279 | { 280 | option: '0.2', 281 | label: '0.2' 282 | }, 283 | { 284 | option: '0.3', 285 | label: '0.3' 286 | }, 287 | { 288 | option: '0.4', 289 | label: '0.4' 290 | }, 291 | { 292 | option: '0.5', 293 | label: '0.5' 294 | }, 295 | { 296 | option: '0.6', 297 | label: '0.6' 298 | }, 299 | 300 | { 301 | option: '0.7', 302 | label: '0.7' 303 | }, 304 | { 305 | option: '0.8', 306 | label: '0.8' 307 | }, 308 | { 309 | option: '0.9', 310 | 311 | label: '0.9' 312 | }, 313 | { 314 | option: '1', 315 | label: '1' 316 | } 317 | ], 318 | selectedOption: topP 319 | }, 320 | {itemType: 'separator'}, 321 | { 322 | propertyName: 'toggleTitle', 323 | itemType: 'toggle', 324 | tooltip: 'Toggle Title', 325 | isToggled: titleVisible, 326 | icon: `` 327 | }, 328 | { 329 | propertyName: 'toggleSystem', 330 | itemType: 'toggle', 331 | tooltip: 'Toggle System Message', 332 | isToggled: systemMessageVisible, 333 | icon: `` 334 | }, 335 | { 336 | propertyName: 'toggleKey', 337 | itemType: 'toggle', 338 | tooltip: `Toggle ${ 339 | isGPT() ? 'OpenAI Key' : 'Anthropic Key & Proxy' 340 | }`, 341 | isToggled: keyVisible, 342 | icon: `` 343 | }, 344 | {itemType: 'separator'}, 345 | { 346 | propertyName: 'expandCollapse', 347 | itemType: 'action', 348 | tooltip: [systemMessage, ...messages].some( 349 | (msg) => !msg.collapsed 350 | ) 351 | ? 'Collapse All Messages' 352 | : 'Expand All Messages', 353 | icon: [systemMessage, ...messages].some((msg) => !msg.collapsed) 354 | ? `` 355 | : `` 356 | }, 357 | { 358 | propertyName: 'widenNarrow', 359 | itemType: 'action', 360 | tooltip: widened ? 'Narrow Chat' : 'Widen Chat', 361 | icon: widened 362 | ? `` 363 | : `` 364 | }, 365 | { 366 | itemType: 'color-selector', 367 | propertyName: 'color', 368 | tooltip: 'Chat Color', 369 | options: [ 370 | {tooltip: 'Red', option: '#F24822'}, 371 | {tooltip: 'Yellow', option: '#FFCD29'}, 372 | {tooltip: 'Green', option: '#14AE5C'}, 373 | {tooltip: 'Blue', option: '#0D99FF'}, 374 | {tooltip: 'Purple', option: '#9747FF'}, 375 | {tooltip: 'Orange', option: '#FFA629'}, 376 | {tooltip: 'Gray', option: '#B3B3B3'}, 377 | {tooltip: 'Default', option: '#ffffff'} 378 | ], 379 | selectedOption: colorState 380 | }, 381 | {itemType: 'separator'}, 382 | { 383 | propertyName: 'resetChat', 384 | itemType: 'action', 385 | tooltip: 'Reset Chat', 386 | icon: `` 387 | } 388 | ], 389 | async ({propertyName, propertyValue}) => { 390 | switch (propertyName) { 391 | case 'toggleTitle': 392 | setTitleVisible((v) => !v) 393 | break 394 | case 'toggleSystem': 395 | setSystemMessageVisible((v) => !v) 396 | break 397 | case 'toggleKey': 398 | setKeyVisible((v) => !v) 399 | break 400 | case 'resetChat': 401 | setSystemMessage({ 402 | role: 'system', 403 | content: '', 404 | collapsed: false 405 | }) 406 | setMessages([ 407 | { 408 | role: 'user', 409 | content: '', 410 | collapsed: false 411 | } 412 | ]) 413 | break 414 | case 'expandCollapse': 415 | // If any messages are expanded, collapse them all 416 | // Otherwise, expand them all 417 | setSystemMessage((msg) => ({ 418 | ...msg, 419 | collapsed: messages.some((msg) => !msg.collapsed) 420 | })) 421 | 422 | setMessages((messages) => 423 | messages.map((msg) => ({ 424 | ...msg, 425 | collapsed: messages.some((msg) => !msg.collapsed) 426 | })) 427 | ) 428 | break 429 | case 'widenNarrow': 430 | setWidened((v) => !v) 431 | break 432 | case 'color': 433 | if (propertyValue !== undefined) 434 | setColorState(propertyValue) 435 | break 436 | case 'model': 437 | if (propertyValue !== undefined) { 438 | // Update model 439 | setModel(propertyValue) 440 | 441 | // If we're switching between GPT and Claude with default temps set, update the temp 442 | if ( 443 | model.includes('gpt') && 444 | !propertyValue.includes('gpt') 445 | ) { 446 | // GPT to Claude 447 | if (temp === '0.7') setTemp('1') 448 | } else if ( 449 | // Claude to GPT 450 | !model.includes('gpt') && 451 | propertyValue.includes('gpt') 452 | ) { 453 | if (temp === '1') setTemp('0.7') 454 | } 455 | } 456 | break 457 | case 'temp': 458 | if (propertyValue !== undefined) setTemp(propertyValue) 459 | break 460 | case 'topP': 461 | if (propertyValue !== undefined) setTopP(propertyValue) 462 | break 463 | } 464 | } 465 | ) 466 | 467 | const addMessage = () => { 468 | setMessages([ 469 | ...messages, 470 | { 471 | role: 472 | messages[messages.length - 1]?.role === 'user' 473 | ? 'assistant' 474 | : 'user', 475 | content: '', 476 | collapsed: false 477 | } 478 | ]) 479 | } 480 | 481 | const cancel = async () => { 482 | setLoadState('ready') 483 | figma.ui.postMessage({type: 'cancel'}) 484 | } 485 | 486 | const submit = async (messagesOverride?: ChatMessage[]) => { 487 | let messagesToSubmit = messagesOverride ?? messages 488 | let openAIKey = openAIKeyMessage.content 489 | let anthropicKey = anthropicKeyMessage.content 490 | let anthropicProxyURL = anthropicProxyURLMessage.content 491 | 492 | // Run Key Fetch if model key not set 493 | if ( 494 | (isGPT() && !openAIKey) || 495 | (!isGPT() && (!anthropicKey || !anthropicProxyURL)) 496 | ) { 497 | console.log('running key fetch') 498 | // Model key not set, run key fetch 499 | const fetchedKeys = keyFetch() 500 | openAIKey = fetchedKeys.openAIKey 501 | anthropicKey = fetchedKeys.anthropicKey 502 | anthropicProxyURL = fetchedKeys.anthropicProxyURL 503 | 504 | // If still no keys, return 505 | if ((isGPT() && !openAIKey) || (!isGPT() && !anthropicKey)) { 506 | setLoadState('error') 507 | setError({ 508 | message: `No ${ 509 | isGPT() ? 'OpenAI' : 'Anthropic' 510 | } key set.\nClick the key icon in the FigChat toolbar to set one.` 511 | }) 512 | return 513 | } else if (!isGPT() && !anthropicProxyURL) { 514 | setLoadState('error') 515 | setError({ 516 | message: `No Proxy URL set.\nFigChat requires a proxy to call Anthropic Claude.\nClick the key icon in the FigChat toolbar to set one.` 517 | }) 518 | return 519 | } 520 | } 521 | 522 | // Submit 523 | const stream = true 524 | setLoadState('loading') 525 | waitForTask( 526 | new Promise((resolve) => { 527 | figma.showUI(__html__, {visible: false}) 528 | figma.ui.postMessage({ 529 | type: stream ? 'submit-stream' : 'submit', 530 | messages: systemMessage.content 531 | ? [systemMessage, ...messagesToSubmit] 532 | : messagesToSubmit, 533 | key: isGPT() ? openAIKey : anthropicKey, 534 | temp: +temp, 535 | topP: +topP, 536 | model, 537 | anthropicProxyURL: isGPT() 538 | ? undefined 539 | : anthropicProxyURLMessage.content 540 | }) 541 | 542 | if (stream) { 543 | // STREAMING 544 | figma.ui.onmessage = async (response: { 545 | messages?: ChatMessage[] 546 | error?: Record 547 | state?: 'streaming' | 'complete' 548 | }) => { 549 | if (response.error) { 550 | setLoadState('error') 551 | setError(response.error) 552 | // @ts-ignore 553 | resolve() 554 | } else if (response.messages) { 555 | setMessages(response.messages) 556 | if (response.state === 'complete') { 557 | setLoadState('ready') 558 | // @ts-ignore 559 | resolve() 560 | } 561 | } 562 | } 563 | } 564 | 565 | if (!stream) { 566 | // NON-STREAMING 567 | figma.ui.onmessage = async (response: { 568 | assistantMessage?: string 569 | error?: Record 570 | }) => { 571 | if (response.error) { 572 | setLoadState('error') 573 | setError(response.error) 574 | } else if (response.assistantMessage) { 575 | const newMessages = [ 576 | ...messagesToSubmit, 577 | { 578 | role: 'assistant', 579 | content: response.assistantMessage, 580 | collapsed: false 581 | } 582 | ] 583 | setMessages(newMessages) 584 | setLoadState('ready') 585 | } 586 | // @ts-ignore 587 | resolve() 588 | } 589 | } 590 | }) 591 | ) 592 | } 593 | 594 | return ( 595 | 613 | {titleVisible && ( 614 | 620 | 626 | setTitle(e.characters)} 630 | placeholder="Chat" 631 | horizontalAlignText="center" 632 | fontSize={35} 633 | fontWeight={600} 634 | lineHeight={46} 635 | fill={color() ?? '#000000'} 636 | /> 637 | { 649 | const oldModel = model 650 | let newModel = '' 651 | 652 | // Rotate model 653 | switch (model) { 654 | case 'gpt-3.5-turbo-16k': 655 | newModel = 'gpt-4' 656 | break 657 | case 'gpt-4': 658 | newModel = 'claude-v1-100k' 659 | break 660 | case 'claude-v1-100k': 661 | newModel = 'claude-instant-v1-100k' 662 | break 663 | case 'claude-instant-v1-100k': 664 | newModel = 'gpt-3.5-turbo-16k' 665 | break 666 | } 667 | setModel(newModel) 668 | 669 | // If we're switching between GPT and Claude with default temps set, update the temp 670 | if ( 671 | oldModel.includes('gpt') && 672 | !newModel.includes('gpt') 673 | ) { 674 | // GPT to Claude 675 | if (temp === '0.7') setTemp('1') 676 | } else if ( 677 | // Claude to GPT 678 | !oldModel.includes('gpt') && 679 | newModel.includes('gpt') 680 | ) { 681 | if (temp === '1') setTemp('0.7') 682 | } 683 | }} 684 | > 685 | 695 | {{ 696 | 'gpt-3.5-turbo-16k': 'GPT-3.5', 697 | 'gpt-4': 'GPT-4', 698 | 'claude-v1-100k': 'Claude v1', 699 | 'claude-instant-v1-100k': 700 | 'Claude Instant v1', 701 | 'claude-2': 'Claude v2' 702 | }[model] ?? 'Unknown'} 703 | 704 | 705 | 706 | 712 | 713 | )} 714 | {keyVisible && ( 715 | { 725 | isGPT() 726 | ? setOpenAIKeyMessage({ 727 | ...openAIKeyMessage, 728 | content 729 | }) 730 | : setAnthropicKeyMessage({ 731 | ...anthropicKeyMessage, 732 | content 733 | }) 734 | }} 735 | onExpandCollapse={(collapsed: boolean) => { 736 | isGPT() 737 | ? setOpenAIKeyMessage({ 738 | ...openAIKeyMessage, 739 | collapsed 740 | }) 741 | : setAnthropicKeyMessage({ 742 | ...anthropicKeyMessage, 743 | collapsed 744 | }) 745 | }} 746 | /> 747 | )} 748 | {keyVisible && !isGPT() && ( 749 | { 759 | setAnthropicProxyURLMessage({ 760 | ...anthropicProxyURLMessage, 761 | content 762 | }) 763 | }} 764 | onExpandCollapse={(collapsed: boolean) => { 765 | setAnthropicProxyURLMessage({ 766 | ...anthropicProxyURLMessage, 767 | collapsed 768 | }) 769 | }} 770 | /> 771 | )} 772 | {systemMessageVisible && ( 773 | { 782 | setSystemMessage({...systemMessage, content}) 783 | }} 784 | onExpandCollapse={(collapsed: boolean) => { 785 | setSystemMessage({...systemMessage, collapsed}) 786 | }} 787 | /> 788 | )} 789 | {messages.map((message, index) => ( 790 | { 799 | const newMessages = [...messages] 800 | newMessages[index].content = content 801 | setMessages(newMessages) 802 | }} 803 | onDelete={() => { 804 | const newMessages = [...messages] 805 | newMessages.splice(index, 1) 806 | setMessages(newMessages) 807 | }} 808 | onExpandCollapse={(collapsed: boolean) => { 809 | const newMessages = [...messages] 810 | newMessages[index].collapsed = collapsed 811 | setMessages(newMessages) 812 | }} 813 | onToggleRole={(role: string) => { 814 | const newMessages = [...messages] 815 | newMessages[index].role = role 816 | setMessages(newMessages) 817 | }} 818 | /> 819 | ))} 820 | 821 | 827 | 837 | `} 839 | /> 840 | 850 | Add message 851 | 852 | 853 | { 866 | const oldModel = model 867 | let newModel = '' 868 | 869 | // Rotate model 870 | switch (model) { 871 | case 'gpt-3.5-turbo-16k': 872 | newModel = 'gpt-4' 873 | break 874 | case 'gpt-4': 875 | newModel = 'claude-v1-100k' 876 | break 877 | case 'claude-v1-100k': 878 | newModel = 'claude-instant-v1-100k' 879 | break 880 | case 'claude-instant-v1-100k': 881 | newModel = 'gpt-3.5-turbo-16k' 882 | break 883 | } 884 | setModel(newModel) 885 | 886 | // If we're switching between GPT and Claude with default temps set, update the temp 887 | if ( 888 | oldModel.includes('gpt') && 889 | !newModel.includes('gpt') 890 | ) { 891 | // GPT to Claude 892 | if (temp === '0.7') setTemp('1') 893 | } else if ( 894 | // Claude to GPT 895 | !oldModel.includes('gpt') && 896 | newModel.includes('gpt') 897 | ) { 898 | if (temp === '1') setTemp('0.7') 899 | } 900 | }} 901 | > 902 | 913 | {{ 914 | 'gpt-3.5-turbo-16k': 'GPT-3.5', 915 | 'gpt-4': 'GPT-4', 916 | 'claude-v1-100k': 'Claude v1', 917 | 'claude-instant-v1-100k': 'Claude Instant v1', 918 | 'claude-2': 'Claude v2' 919 | }[model] ?? 'Unknown'} 920 | 921 | 922 | 923 | { 933 | // Cancel if running 934 | cancel() 935 | 936 | // Delete last message 937 | const newMessages = [...messages] 938 | newMessages.pop() 939 | setMessages(newMessages) 940 | 941 | // Submit 942 | submit(newMessages) 943 | }} 944 | > 945 | 950 | 959 | Redo 960 | 961 | 962 | submit() : cancel} 966 | verticalAlignItems="center" 967 | horizontalAlignItems="center" 968 | padding={{vertical: 6, horizontal: 8}} 969 | spacing={6} 970 | hoverStyle={ 971 | openAIKeyMessage.content 972 | ? { 973 | fill: 974 | loadState === 'loading' 975 | ? '#B50808' 976 | : '#832DDA' 977 | } 978 | : {} 979 | } 980 | > 981 | {loadState !== 'loading' && ( 982 | `} 984 | /> 985 | )} 986 | {loadState === 'loading' && ( 987 | `} 989 | /> 990 | )} 991 | 999 | {loadState === 'loading' ? 'Cancel' : 'Submit'} 1000 | 1001 | 1002 | 1003 | {loadState === 'error' && ( 1004 | 1013 | `} 1015 | /> 1016 | 1025 | {error.message} 1026 | 1027 | setLoadState('ready')} 1036 | > 1037 | 1047 | OK 1048 | 1049 | 1050 | 1051 | )} 1052 | 1053 | ) 1054 | } 1055 | 1056 | widget.register(FigChat) 1057 | --------------------------------------------------------------------------------