2
0
Fork 0

work on ui, session saving, etc.

This commit is contained in:
Luca Bilke 2024-05-04 20:32:00 +02:00
parent 1fdfa696d8
commit 96ce5c4023
No known key found for this signature in database
GPG Key ID: AD6630D0A1E650AC
7 changed files with 269 additions and 147 deletions

View File

@ -1,25 +1,55 @@
local M = {}
---@alias provider_name "anthropic"
---@alias hook_name "request_started" | "request_finished"
M.opts = {
hooks = {
request_started = function() end,
request_finished = function() end,
},
api_url = "https://api.anthropic.com/v1/messages",
border_style = "single",
layout = {
relative = "editor",
position = {
row = "50%",
col = "50%",
},
size = {
width = "80%",
height = "90%",
},
},
token = nil,
nerdfonts = false,
}
M.state = {
num_callbacks = 0,
spinner_index = 0,
}
M.resources = {}
function M.setup(opts)
M.opts = vim.tbl_deep_extend("force", M.opts, opts)
if M.opts.nerdfonts then
M.resources = {
icons = {
user = "",
assistant = "󰚩",
},
spinner = {
fps = 25,
parts = {
"", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
},
},
}
else
M.resources = {
icons = {
user = "User:",
assistant = "Assistant:",
},
spinner = {
fps = 12,
parts = { "", "", "", "", "", "", "", "", "", "" },
},
}
end
end
return M

View File

@ -1,6 +1,5 @@
local VERSION = "2023-06-01"
local opts = require("anthropic").opts
local state = require("anthropic").state
local M = {}
@ -22,7 +21,7 @@ end
--- # Make message request for anthropic
---
--- This will generate the json body for an anthropic message call
---@param messages { role: string, content: string }[]
---@param session session
---@param model string?
---@param max_tokens integer?
---@param system string?
@ -30,12 +29,12 @@ end
---@param top_k integer?
---@param top_p integer?
---@return table<string, any>?
function M.make_body(messages, model, max_tokens, system, temperature, top_k, top_p)
function M.make_body(session, model, max_tokens, system, temperature, top_k, top_p)
model = model or "claude-3-sonnet-20240229"
max_tokens = max_tokens or 1024
return {
messages = messages,
messages = session,
model = model,
max_tokens = max_tokens,
system = system,
@ -45,22 +44,6 @@ function M.make_body(messages, model, max_tokens, system, temperature, top_k, to
}
end
--- # Run hook
---
--- This function runs a registered hook
---@param hook hook_name
local function run_hook(hook)
local p = state
if hook == "request_started" then
p.num_callbacks = p.num_callbacks + 1
elseif hook == "request_finished" then
p.num_callbacks = p.num_callbacks - 1
end
pcall(opts.hooks[hook])
end
--- # Curl exit code test
---
--- Tests the curl exit code and notifies the user if something went wrong.
@ -68,10 +51,10 @@ end
---@return boolean
function M.test_code(exit_code)
if exit_code == 127 then
vim.notify("anthropic.util couldn't find curl binary in path", vim.log.levels.ERROR)
vim.notify("anthropic: couldn't find curl binary in path", vim.log.levels.ERROR)
return false
elseif exit_code ~= 0 then
vim.notify("anthropic.util unkown curl exit code: " .. exit_code, vim.log.levels.ERROR)
vim.notify("anthropic: unkown curl exit code: " .. exit_code, vim.log.levels.ERROR)
return false
end
return true
@ -80,7 +63,7 @@ end
--- # Curl callback
---
--- This function wraps the callback passed to transfarmer.util.call to perform
--- error checking, json decoding and hook execution.
--- error checking and json decoding
---@param response curl_response
---@param callback curl_callback
local function callback_wrapper(response, callback)
@ -91,7 +74,7 @@ local function callback_wrapper(response, callback)
end
if response.body == nil or response.body == "" then
vim.notify("anthropic.util empty response body", vim.log.levels.ERROR)
vim.notify("anthropic: empty response body", vim.log.levels.ERROR)
return
end
@ -100,11 +83,9 @@ local function callback_wrapper(response, callback)
if ok then
callback(result)
else
vim.notify("anthropic.util failed to decode json: " .. result, vim.log.levels.ERROR)
vim.notify("anthropic: failed to decode json: " .. result, vim.log.levels.ERROR)
end
end)(response.body)
run_hook("request_finished")
end
--- # Call a anthropic's API
@ -118,8 +99,6 @@ function M.call(payload, callback)
local url = opts.api_url
local headers = M.make_headers()
opts.hooks.request_started()
require("plenary.curl").post(url, {
body = payload_str,
headers = headers,
@ -127,8 +106,7 @@ function M.call(payload, callback)
callback_wrapper(response, callback)
end,
on_error = function(err)
vim.notify("anthropic.util curl error from: " .. err.message, vim.log.levels.WARNING)
run_hook("request_finished")
vim.notify("anthropic: curl error from: " .. err.message, vim.log.levels.WARNING)
end,
})
end

View File

@ -0,0 +1,132 @@
if not pcall(require, "nui.layout") then
vim.notify("nui.nvim not installed!", vim.log.levels.ERROR)
return
end
local M = {}
local opts = require("anthropic").opts
local layout = require("nui.layout")
local popup = require("nui.popup")
M.components = {
history = popup({
border = {
style = opts.border_style,
},
enter = false,
}),
input = popup({
border = {
style = opts.border_style,
},
enter = true,
})
}
M.box = layout(
{
relative = "editor",
position = {
row = "50%",
col = "50%",
},
size = {
width = "80%",
height = "90%",
},
},
layout.Box(
{
layout.Box(M.components.history, { size = "70%" }),
layout.Box(M.components.input, { size = "30%" })
},
{ dir = "col" }
)
)
M.active = false
M.initialized = false
M.cursor_y = 0
M.cursor_x = 0
--- # Set up the chat window
---
--- Adds autocommands and mounts the chat window
local function initialize()
if M.initialized then
return
end
M.box:mount()
-- autohide chat when leaving the chat buffer
for _, p in pairs(M.components) do
p:on("BufLeave", function()
vim.schedule(function()
local buf = vim.api.nvim_get_current_buf()
for _, q in pairs(M.components) do
if q.bufnr == buf then
return
end
end
M.box:hide()
end)
end)
end
-- automatically resize the chat window on SIGWINCH
vim.api.nvim_create_autocmd("WinResized", {
group = vim.api.nvim_create_augroup("refresh_anthropic_layout", { clear = true }),
callback = function()
M.box:update()
end,
})
M.initialized = true
end
--- # Open the chat window
function M.open()
initialize()
M.box:show()
M.active = true
end
--- # Close the chat window
function M.close()
M.box:hide()
M.active = false
end
--- # Toggle the chat window
function M.toggle()
if M.active then
M.close()
else
M.open()
end
end
--- # Write text to the chat window
---
--- Writing out of the buffer's bounds will cause an error!
---@param text string[] # Each element is a line of text
---@param row integer? # Defaults to last row written to
---@param col integer? # Defaults to last col written to + 1
function M.write(text, row, col)
col = col or M.cursor_x
row = row or M.cursor_y
local buf = M.components.history.bufnr
vim.api.nvim_buf_set_text(buf, row, col, row, col, text)
M.cursor_y = M.cursor_y + #text - 1
if #text > 1 then
M.cursor_x = 0
end
M.cursor_x = M.cursor_x + #text[#text]
end
return M

View File

@ -1,4 +1,4 @@
---@meta [api]
---@meta [util]
---@alias curl_callback fun(deserialized: table<string, any>)
@ -21,3 +21,10 @@
---@field status integer The https response status
---@field headers table The https response headers
---@field body string The http response body
---@alias message { role: "user" | "assistant", content: string }
---@class session
---@field name string
---@field messages message[]
---@field system_prompt string?

View File

@ -0,0 +1,74 @@
local M = {}
local session_dir = vim.fn.stdpath("data")
---@param name string
---@return string
local function filename(name)
return session_dir .. "/" .. name .. ".json"
end
--- # Save a session
---
--- Encodes a session in json and writes it to disk
---@param session session
function M.save(session)
local path = filename(session.name)
local json = vim.fn.json_encode(session)
vim.fn.writefile({ json }, path)
end
--- # Validate a session's messages
---
--- Tests the order of message roles
---@param messages message[]
---@return boolean
function M.validate_messages(messages)
local want_user = true
for _, message in ipairs(messages) do
if message.role == "assistant" then
if not want_user then
want_user = true
else
return false
end
elseif message.role == "user" then
if want_user then
want_user = false
else
return false
end
else
return false
end
end
return true
end
--- # Load a session
---
--- Decodes a session from a jsonfile
---@param name string
---@return session? # nil if the session is invalid
function M.load(name)
local path = filename(name)
local json = table.concat(vim.fn.readfile(path), "\n")
local session = vim.fn.json_decode(json)
if M.validate_messages(session) then
return session
end
end
--- # List saved sessions
---
--- Lists filenames of saved sessions without the .json extension
---@return string[]
function M.list()
local sessions = vim.fn.glob(session_dir .. "/*.json", false, true)
for i, session in ipairs(sessions) do
sessions[i] = string.match(session, "([^/]+)%.json$")
end
return sessions
end
return M

View File

@ -1,47 +1,16 @@
local resources = require("anthropic").resources
local M = {}
local spinner = {
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
}
M.index = 0
--- # Get spinner
---
--- This function gets the current spinner icon
---@return string spinner
function M.get_spinner()
local state = require("anthropic").state
if state.num_callbacks == 0 then
return ""
end
state.spinner_index = state.spinner_index + 1
return spinner[state.spinner_index % #spinner + 1]
M.index = M.index + 1
return resources.spinner[M.index % #resources.spinner + 1]
end
return M

View File

@ -1,68 +0,0 @@
local M = {}
local opts = require("anthropic.opts")
local layout = require("nui.layout")
local popup = require("nui.popup")
local popups = {
response = popup({
border = {
style = opts.border_style,
},
enter = false,
buf_options = {
modifiable = false,
readonly = true,
},
}),
input = popup({
border = {
style = opts.border_style,
},
enter = true,
buf_options = {
modifiable = true,
readonly = false,
},
})
}
local chat = layout(
{
relative = "editor",
position = {
row = "20%",
column = "10%",
},
size = {
width = "80%",
height = "90%",
},
},
layout.Box(
{
layout.Box(popups.response, { size = "60%" }),
layout.Box(popups.input, { size = "30%" })
},
{ dir = "col" }
)
)
for _, p in pairs(popups) do
p:on("BufLeave", function()
vim.schedule(function()
local buf = vim.api.nvim_get_current_buf()
for _, q in pairs(popups) do
if q.bufnr == buf then
return
end
end
layout:hide()
end)
end)
end
function M.open()
end
return M