├── web
├── public
│ └── favicon.png
├── src
│ ├── app
│ │ ├── api
│ │ │ ├── groq
│ │ │ │ ├── allowedModels.js
│ │ │ │ ├── route.js
│ │ │ │ └── [...path]
│ │ │ │ │ └── route.js
│ │ │ ├── mcp-auth
│ │ │ │ └── callback
│ │ │ │ │ └── route.js
│ │ │ └── proxy
│ │ │ │ └── route.js
│ │ ├── page.jsx
│ │ ├── layout.jsx
│ │ └── oauth
│ │ │ └── callback
│ │ │ └── page.jsx
│ ├── ui
│ │ ├── builtinTools.js
│ │ ├── ScriptEditor.jsx
│ │ ├── spreadsheetMcp.js
│ │ ├── mcpClient.js
│ │ ├── FileManager.jsx
│ │ └── Grid.jsx
│ └── styles.css
├── next.config.mjs
└── package.json
├── src
├── index.js
└── lib
│ ├── errors.js
│ ├── builtins
│ ├── utils.js
│ └── index.js
│ ├── registry.js
│ ├── parser.js
│ └── engine.js
├── package.json
├── .github
└── workflows
│ ├── stale.yaml
│ └── code-freeze-bypass.yaml
├── test
├── groqProxy.test.js
└── engine.test.js
├── README.md
├── .gitignore
└── LICENSE
/web/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/groq/groq-autosheet/HEAD/web/public/favicon.png
--------------------------------------------------------------------------------
/web/src/app/api/groq/allowedModels.js:
--------------------------------------------------------------------------------
1 | export const ALLOWED_MODELS = new Set([
2 | 'openai/gpt-oss-20b',
3 | 'openai/gpt-oss-120b',
4 | ]);
5 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { SpreadsheetEngine } from './lib/engine.js';
2 | export { registerBuiltins } from './lib/builtins/index.js';
3 | export { getBuiltinFunctionNames } from './lib/registry.js';
4 |
5 |
6 |
--------------------------------------------------------------------------------
/web/src/app/page.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import React from 'react'
3 | import dynamic from 'next/dynamic'
4 | import '../styles.css'
5 |
6 | const AppNoSSR = dynamic(() => import('../ui/App.jsx'), { ssr: false })
7 |
8 | export default function Page() {
9 | return
Completing authentication… You can close this window.
} 96 | {status === 'success' &&Authentication successful! This window will close automatically.
} 97 | {status === 'error' && ( 98 |Authentication failed:
100 |{error}
101 | You can close this window and try again.
102 || Name | 452 |Modified | 453 |Actions | 454 |
|---|---|---|
| 464 | {renamingId === file.id ? ( 465 | setRenamingValue(e.target.value)} 469 | onBlur={commitRename} 470 | onKeyDown={(e) => { 471 | if (e.key === 'Enter') commitRename() 472 | if (e.key === 'Escape') setRenamingId(null) 473 | }} 474 | onClick={(e) => e.stopPropagation()} 475 | autoFocus 476 | /> 477 | ) : ( 478 | 479 | {file.name}.as 480 | {getCurrentFileId() === file.id && (current)} 481 | 482 | )} 483 | | 484 |{formatDate(file.updatedAt)} | 485 |
486 |
487 |
490 |
493 |
499 |
500 | |
501 |
| 647 | {Array.from({ length: cols }, (_, c) => ( 648 | |
649 | {colLabel(c + 1)}
650 | {
653 | e.preventDefault()
654 | e.stopPropagation()
655 | beginColumnResize(c, e.clientX)
656 | }}
657 | onDoubleClick={(e) => {
658 | e.preventDefault()
659 | e.stopPropagation()
660 | autoFitColumn(c)
661 | }}
662 | />
663 | |
664 | ))}
665 |
|---|---|
|
671 | {r + 1}
672 | {
675 | e.preventDefault()
676 | e.stopPropagation()
677 | beginRowResize(r, e.clientY)
678 | }}
679 | onDoubleClick={(e) => {
680 | e.preventDefault()
681 | e.stopPropagation()
682 | autoFitRow(r)
683 | }}
684 | />
685 | |
686 | {Array.from({ length: cols }, (_, c) => {
687 | const rr = r + 1
688 | const cc = c + 1
689 | const isAnchor = selection.row === rr && selection.col === cc
690 | const hasFocus = !!selection.focus
691 | const top = hasFocus ? Math.min(selection.row, selection.focus.row) : selection.row
692 | const left = hasFocus ? Math.min(selection.col, selection.focus.col) : selection.col
693 | const bottom = hasFocus ? Math.max(selection.row, selection.focus.row) : selection.row
694 | const right = hasFocus ? Math.max(selection.col, selection.focus.col) : selection.col
695 | const inRange = hasFocus && rr >= top && rr <= bottom && cc >= left && cc <= right
696 | const className = hasFocus
697 | ? (inRange ? (isAnchor ? 'sel-range sel-anchor' : 'sel-range') : '')
698 | : (isAnchor ? 'sel' : '')
699 | const isEditing = editing && editing.row === rr && editing.col === cc
700 | return (
701 | {
707 | // If clicking inside the active input, allow caret placement and do not exit editing
708 | if (isEditing && e.target && e.target.tagName === 'INPUT') {
709 | return
710 | }
711 | e.preventDefault()
712 | if (e.shiftKey) {
713 | // Expand from existing anchor to this cell
714 | setSelection({ row: selection.row, col: selection.col, focus: { row: rr, col: cc } })
715 | } else {
716 | setSelection({ row: rr, col: cc })
717 | isSelectingRef.current = true
718 | dragStartRef.current = { row: rr, col: cc }
719 | }
720 | if (tableRef.current) tableRef.current.focus()
721 | }}
722 | onMouseEnter={() => {
723 | if (isSelectingRef.current && dragStartRef.current) {
724 | const start = dragStartRef.current
725 | setSelection({ row: start.row, col: start.col, focus: { row: rr, col: cc } })
726 | }
727 | }}
728 | onDoubleClick={(e) => {
729 | setSelection({ row: rr, col: cc })
730 | const raw = getCellRaw ? getCellRaw(rr, cc) : ''
731 | startEditing(rr, cc, raw ?? '')
732 | }}
733 | >
734 | {isEditing ? (
735 | setEditValue(e.target.value)}
739 | onMouseDown={(e) => { e.stopPropagation() }}
740 | onDoubleClick={(e) => { e.stopPropagation() }}
741 | onKeyDown={(e) => {
742 | e.stopPropagation()
743 | if (e.key === 'Enter') {
744 | e.preventDefault()
745 | commitEditing(rr, cc, editValue, e.shiftKey ? 'up' : 'down')
746 | } else if (e.key === 'Escape') {
747 | e.preventDefault()
748 | cancelEditing()
749 | } else if (e.key === 'Tab') {
750 | e.preventDefault()
751 | commitEditing(rr, cc, editValue, e.shiftKey ? 'left' : 'right')
752 | }
753 | }}
754 | onBlur={() => commitEditing(rr, cc, editValue)}
755 | style={{ width: '100%', height: '100%', boxSizing: 'border-box', border: 'none', outline: 'none', font: 'inherit', padding: '0 1px', margin: 0 }}
756 | />
757 | ) : (
758 |
759 | {
765 | const format = getCellFormat && getCellFormat(rr, cc)
766 | if (!format) return 'none'
767 | const decorations = []
768 | if (format.underline) decorations.push('underline')
769 | if (format.strikethrough) decorations.push('line-through')
770 | return decorations.length > 0 ? decorations.join(' ') : 'none'
771 | })()
772 | }}
773 | >
774 | {getCellDisplay(rr, cc)}
775 |
776 |
777 | )}
778 | |
779 | )
780 | })}
781 |