work on ui, session saving, etc.
This commit is contained in:
parent
1fdfa696d8
commit
96ce5c4023
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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?
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue