From 76fab5c716c1c77124da1d74b05ecbd4d8e47516 Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Thu, 21 May 2026 17:04:34 -0400 Subject: [PATCH] fix: save uploaded files to recipe-images/ subdir and inject source URL --- src/Roux/Server.hs | 57 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/Roux/Server.hs b/src/Roux/Server.hs index 4a61fd5..d9a58d5 100644 --- a/src/Roux/Server.hs +++ b/src/Roux/Server.hs @@ -384,14 +384,15 @@ runFileImport recipeDir config originalName _mimeType b64Text = do Html.ImportError $ "Could not decode uploaded file: " <> T.pack err Right content -> do - -- Ensure recipe dir exists - createDirectoryIfMissing True recipeDir - -- Save the original file - let sourceFilename = map (\c -> if isAlphaNum c || c == '.' || c == '-' || c == '_' then c else '_') (T.unpack originalName) - sourcePath = recipeDir sourceFilename + -- Save the original file to recipe-images/ subdirectory + let imagesDir = recipeDir "recipe-images" + safeName = map (\c -> if isAlphaNum c || c == '.' || c == '-' || c == '_' then c else '_') (T.unpack originalName) + sourcePath = imagesDir safeName + sourceUrl = "/recipe-images/" <> T.pack safeName + createDirectoryIfMissing True imagesDir BS.writeFile sourcePath content putStrLn $ "[roux] saved uploaded file to " <> sourcePath - -- Call the LLM pipeline with the saved file + -- Call the LLM pipeline with the source URL case rcAnthropic config of Nothing -> pure $ @@ -399,7 +400,7 @@ runFileImport recipeDir config originalName _mimeType b64Text = do Html.ImportError $ "LLM import is not configured. Add an anthropic.api_key to the config file." Just anthropicConfig -> do - result <- runLLMPipeline recipeDir anthropicConfig (Just sourcePath) "" + result <- runLLMPipeline recipeDir anthropicConfig (Just sourceUrl) (Just sourcePath) "" case result of Right filename -> pure $ Right filename Left err -> pure $ Left err @@ -418,7 +419,7 @@ runTextImport recipeDir config text = do Html.ImportError $ "LLM import is not configured. Add an anthropic.api_key to the config file." Just anthropicConfig -> - runLLMPipeline recipeDir anthropicConfig Nothing text + runLLMPipeline recipeDir anthropicConfig Nothing Nothing text -- | Run the full LLM import pipeline: call convert-to-cooklang -> parse -> save. runLLMPipeline :: @@ -426,19 +427,21 @@ runLLMPipeline :: FilePath -> -- | API config AnthropicConfig -> - -- | Just path = uploaded source file to keep + -- | Just url = source URL to inject into Cooklang front matter + Maybe Text -> + -- | Just path = file path for the script (file upload) / Nothing = use text Maybe FilePath -> - -- | recipe text (or empty if file) + -- | recipe text (used when no file path) Text -> IO (Either Html.ImportError FilePath) -runLLMPipeline recipeDir anthropicConfig mSourceFile text = do +runLLMPipeline recipeDir anthropicConfig mSourceUrl mFilePath text = do -- Step 1: locate the script script <- findConvertScript -- Step 2: write a temp config for the subprocess let configPath = recipeDir ".convert-config.yaml" writeConvertConfig configPath anthropicConfig -- Step 3: build subprocess args - let args = case mSourceFile of + let args = case mFilePath of Just fp -> ["--config", configPath, "--file", fp] Nothing -> ["--config", configPath, "--text", T.unpack text] putStrLn $ "[roux] running " <> script <> " for LLM import" @@ -456,7 +459,10 @@ runLLMPipeline recipeDir anthropicConfig mSourceFile text = do "Could not convert recipe: " <> errMsg ExitSuccess -> do -- Step 4: parse the Cooklang output to extract the title - let cooklangText = TE.decodeUtf8 (LB.toStrict stdoutBytes) + let rawCooklang = TE.decodeUtf8 (LB.toStrict stdoutBytes) + cooklangText = case mSourceUrl of + Just url -> injectSourceUrl rawCooklang url + Nothing -> rawCooklang case parseRecipeTitle cooklangText of Nothing -> do putStrLn "[roux] could not extract title from Cooklang output" @@ -469,7 +475,7 @@ runLLMPipeline recipeDir anthropicConfig mSourceFile text = do filepath = recipeDir filename -- Step 5: write the .cook file putStrLn $ "[roux] writing imported recipe to " <> filepath - LB.writeFile filepath stdoutBytes + BS.writeFile filepath (TE.encodeUtf8 cooklangText) putStrLn $ "[roux] successfully imported recipe as " <> filename return $ Right filename @@ -500,6 +506,29 @@ writeConvertConfig path ac = do Nothing -> [] writeFile path (unlines pairs) +{- | Insert or update the 'source:' field in Cooklang YAML front matter. +If a source field already exists, its value is replaced. Otherwise a new +source line is added before the closing '---'. +-} +injectSourceUrl :: Text -> Text -> Text +injectSourceUrl cooklang url = + case T.breakOn "---" cooklang of + (before, rest) -> + let afterFirst = T.drop 3 rest + (frontMatter, afterSecond) = T.breakOn "---" afterFirst + updated = + let (beforeSource, afterSource) = T.breakOn "source:" frontMatter + in if T.null afterSource + -- No existing source field: append one + then + let trimmed = T.stripEnd frontMatter + in trimmed <> "\nsource: " <> url <> "\n" + -- Existing source field: replace its value + else + let restOfLine = T.drop 1 (T.dropWhile (/= '\n') afterSource) + in beforeSource <> "source: " <> url <> restOfLine + in before <> "---" <> updated <> "---" <> afterSecond + {- | Parse the recipe title from Cooklang front matter. Looks for "title: ..." in the YAML front matter block. -}