diff --git a/content/content_api_handler.go b/content/content_api_handler.go index cde9f53..1580a90 100644 --- a/content/content_api_handler.go +++ b/content/content_api_handler.go @@ -68,7 +68,18 @@ func ContentUpdateAPIHandler(w http.ResponseWriter, req *http.Request) { return } - UpdateContent(body.Path, body.Type, body.Format, body.Content) + if body.Type == "notebook" { + UpdateNbContent(body.Path, body.Type, body.Format, body.Content) + } + + if body.Type == "file" { + contentStr, ok := body.Content.(string) + if !ok { + http.Error(w, "Invalid content type", http.StatusBadRequest) + return + } + UpdateContent(body.Path, body.Type, body.Format, contentStr) + } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/content/content_manager.go b/content/content_manager.go index 084569c..2c55d0d 100644 --- a/content/content_manager.go +++ b/content/content_manager.go @@ -295,7 +295,56 @@ func GetOSPath(path string) string { return abspath } -// save content +func UpdateNbContent(path, ftype, format string, content interface{}) error { + var nb OutNotebook + log.Info().Msgf("Updating notebook content for path: %s", path) + + // Convert content to JSON if it's a string or []byte, otherwise directly marshal it + var contentBytes []byte + var err error + + switch v := content.(type) { + case string: + // If content is a string, assume it's JSON and convert it to []byte + contentBytes = []byte(v) + case []byte: + // If content is already []byte, assume it's JSON + contentBytes = v + case map[string]interface{}: + // If content is already a map, we can directly marshal it into the notebook + contentBytes, err = json.Marshal(content) + if err != nil { + return fmt.Errorf("failed to marshal map content into JSON: %w", err) + } + default: + // If the content is an unsupported type + return fmt.Errorf("content is not a valid type (expected string, []byte, or map[string]interface{}), got: %T", content) + } + + // Unmarshal the JSON bytes into the Notebook struct + if err := json.Unmarshal(contentBytes, &nb); err != nil { + return fmt.Errorf("failed to unmarshal content into notebook: %w", err) + } + + newNb := splitLines(nb) + + // Marshal the notebook struct back into JSON (to save the updated notebook) + nbJSON, err := json.Marshal(newNb) + if err != nil { + return fmt.Errorf("failed to marshal notebook: %w", err) + } + + log.Info().Msgf("nbJSON: %s", string(nbJSON)) + + // Write the JSON back to the file + if err := os.WriteFile(path, nbJSON, 0644); err != nil { + log.Error().Err(err).Msgf("Error updating notebook content for path: %s", path) + return fmt.Errorf("error writing notebook to path %s: %w", path, err) + } + + log.Info().Msgf("Successfully updated notebook content for path: %s", path) + return nil +} func UpdateContent(path, ftype, format, content string) error { err := os.WriteFile(path, []byte(content), 0644) diff --git a/content/notebook.go b/content/notebook.go index 5529530..63ed9f3 100644 --- a/content/notebook.go +++ b/content/notebook.go @@ -111,31 +111,62 @@ func _splitMimeBundle(data map[string]interface{}) map[string]interface{} { } // splitLines splits likely multi-line text into lists of strings. -// func splitLines(nb *Notebook) *Notebook { -// for _, cell := range nb.Cells { -// if source, ok := cell.Source.(string); ok { -// cell.Source = strings.SplitAfter(source, "\n") -// } - -// for _, attachment := range cell.Attachments { -// _splitMimeBundle(attachment) -// } - -// if cell.CellType == "code" { -// for _, output := range cell.Outputs { -// switch output.OutputType { -// case "execute_result", "display_data": -// _splitMimeBundle(output.Data) -// case "stream": -// if text, ok := output.Text.(string); ok { -// output.Text = strings.SplitAfter(text, "\n") -// } -// } -// } -// } -// } -// return nb -// } +func splitLines(outNb OutNotebook) Notebook { + nb := Notebook{ + Cells: []Cell{}, + Metadata: outNb.Metadata, + } + for _, outCell := range outNb.Cells { + // Convert string slice to []interface{} + sourceLines := strings.SplitAfter(outCell.Source, "\n") + sourceInterface := make([]interface{}, len(sourceLines)) + for i, v := range sourceLines { + sourceInterface[i] = v + } + + // Convert attachments map + attachments := make(map[string]map[string]interface{}) + for k, v := range outCell.Attachments { + attachments[k] = map[string]interface{}{"data": v} + } + + // Convert outputs + outputs := make([]Output, len(outCell.Outputs)) + for i, out := range outCell.Outputs { + outputs[i] = Output{ + OutputType: out.OutputType, + ExecutionCount: out.ExecutionCount, + Data: map[string]interface{}{"text": out.Data}, + Text: []interface{}{out.Text}, + Metadata: out.Metadata, + } + } + + nb.Cells = append(nb.Cells, Cell{ + Source: sourceInterface, + CellType: outCell.CellType, + ExecutionCount: outCell.ExecutionCount, + Attachments: attachments, + Outputs: outputs, + Metadata: outCell.Metadata, + }) + } + + // if cell.CellType == "code" { + // for _, output := range cell.Outputs { + // switch output.OutputType { + // case "execute_result", "display_data": + // _splitMimeBundle(output.Data) + // case "stream": + // if text, ok := output.Text.(string); ok { + // output.Text = strings.SplitAfter(text, "\n") + // } + // } + // } + // } + return nb + +} // stripTransient removes transient metadata from the notebook. func stripTransient(nb *Notebook) *Notebook { diff --git a/content/payload.go b/content/payload.go index 4fdf8fb..5314aeb 100644 --- a/content/payload.go +++ b/content/payload.go @@ -15,9 +15,9 @@ type ( } ContentUpdateRequest struct { - Path string `json:"path"` - Content string `json:"content"` - Format string `json:"format"` - Type string `json:"type"` + Path string `json:"path"` + Content interface{} `json:"content"` + Format string `json:"format"` + Type string `json:"type"` } ) diff --git a/ui/src/ide/editor/notebook/Cell.tsx b/ui/src/ide/editor/notebook/Cell.tsx index d35a907..5126325 100644 --- a/ui/src/ide/editor/notebook/Cell.tsx +++ b/ui/src/ide/editor/notebook/Cell.tsx @@ -39,6 +39,7 @@ interface ICellProps{ divRefs: React.RefObject<(HTMLDivElement | null)[]>; execution_count: number; codeMirrorRefs: any; + updateCellSource: any; } export interface CodeMirrorRef { @@ -56,6 +57,7 @@ const Cell = React.forwardRef((props: ICellProps, ref) => { const onChange = useCallback((value, viewUpdate) => { setCellContents(value) + props.updateCellSource(value, props.cell.id) }, []); const onUpdate = useCallback((viewUpdate: ViewUpdate) => { diff --git a/ui/src/ide/editor/notebook/NotebookEditor.tsx b/ui/src/ide/editor/notebook/NotebookEditor.tsx index e87c590..092bc6e 100644 --- a/ui/src/ide/editor/notebook/NotebookEditor.tsx +++ b/ui/src/ide/editor/notebook/NotebookEditor.tsx @@ -98,7 +98,7 @@ export default function NotebookEditor(props) { useEffect(() => { if (props.data.load_required === true) { FetchFileData(props.data.path); - const session = startASession(props.data.path, props.data.name, props.data.type); + // const session = startASession(props.data.path, props.data.name, props.data.type); } }, [props.data]); @@ -224,6 +224,20 @@ export default function NotebookEditor(props) { } }; + const updateCellSource = (value: string, cellId: string) => { + + setNotebook((prevNotebook) => { + const updatedCells = prevNotebook.cells.map((cell) => { + if (cell.id === cellId) { + return { ...cell, source: value }; + } + return cell; + }); + + return { ...prevNotebook, cells: updatedCells }; + }); + } + const submitCell = (source: string, cellId: string) => { setNotebook((prevNotebook) => { const updatedCells = prevNotebook.cells.map((cell) => { @@ -341,11 +355,27 @@ export default function NotebookEditor(props) { } }; + const handleCmdEnter = () => { + console.log('Saving notebook') + + fetch(BaseApiUrl + '/api/contents', { + method: 'PUT', + body: JSON.stringify({ + path: props.data.path, + content: notebook, + type: 'notebook', + format: 'json' + }) + }) + + return true + } + return (